You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2024/02/06 15:00:24 UTC

(fineract) branch develop updated: FINERACT-1971: Handling inline COB execution before batch API execution outside of the batch transaction

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

arnold 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 770719e8e FINERACT-1971: Handling inline COB execution before batch API execution outside of the batch transaction
770719e8e is described below

commit 770719e8ecbc40933bdec48e4e505fe60a546c01
Author: Arnold Galovics <ga...@gmail.com>
AuthorDate: Fri Jan 26 09:04:59 2024 +0100

    FINERACT-1971: Handling inline COB execution before batch API execution outside of the batch transaction
---
 .../http/BodyCachingHttpServletRequestWrapper.java |  88 +++++++++++++++
 .../jobs/filter/LoanCOBApiFilter.java              |   7 +-
 .../jobs/filter/LoanCOBBatchPreprocessor.java      |  74 -------------
 .../jobs/filter/LoanCOBFilterHelper.java           |  90 +++++++++++++++-
 .../jobs/filter/LoanCOBApiFilterTest.java          |  34 ++++--
 .../integrationtests/BaseLoanIntegrationTest.java  | 120 +++++++++++++++++++--
 .../fineract/integrationtests/BatchApiTest.java    |  32 +++---
 .../integrationtests/BatchLoanIntegrationTest.java | 101 +++++++++++++++++
 .../integrationtests/common/BatchHelper.java       |  12 +++
 .../common/error/ErrorResponse.java                |  27 +++++
 .../useradministration/users/UserHelper.java       |  20 ++--
 11 files changed, 484 insertions(+), 121 deletions(-)

diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/http/BodyCachingHttpServletRequestWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/http/BodyCachingHttpServletRequestWrapper.java
new file mode 100644
index 000000000..862d2620e
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/http/BodyCachingHttpServletRequestWrapper.java
@@ -0,0 +1,88 @@
+/**
+ * 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.http;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import jakarta.servlet.ReadListener;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequestWrapper;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import org.apache.commons.io.IOUtils;
+
+public class BodyCachingHttpServletRequestWrapper extends HttpServletRequestWrapper {
+
+    private final byte[] cachedBody;
+
+    @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW")
+    public BodyCachingHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
+        super(request);
+        this.cachedBody = IOUtils.toByteArray(request.getInputStream());
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+        return new CachedBodyServletInputStream(cachedBody);
+    }
+
+    @Override
+    public BufferedReader getReader() throws IOException {
+        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody);
+        return new BufferedReader(new InputStreamReader(byteArrayInputStream, UTF_8));
+    }
+
+    public static class CachedBodyServletInputStream extends ServletInputStream {
+
+        private final InputStream inputStream;
+
+        public CachedBodyServletInputStream(byte[] cachedBody) {
+            this.inputStream = new ByteArrayInputStream(cachedBody);
+        }
+
+        @Override
+        public boolean isFinished() {
+            try {
+                return inputStream.available() == 0;
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public boolean isReady() {
+            return true;
+        }
+
+        @Override
+        public void setReadListener(ReadListener listener) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int read() throws IOException {
+            return inputStream.read();
+        }
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java
index 59e4a0855..56a0be0c1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java
@@ -27,6 +27,7 @@ import java.util.List;
 import lombok.RequiredArgsConstructor;
 import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
 import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse;
+import org.apache.fineract.infrastructure.core.http.BodyCachingHttpServletRequestWrapper;
 import org.apache.fineract.infrastructure.jobs.exception.LoanIdsHardLockedException;
 import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException;
 import org.apache.http.HttpStatus;
@@ -62,7 +63,9 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
     @Override
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
             throws ServletException, IOException {
-        if (!helper.isOnApiList(request.getPathInfo(), request.getMethod())) {
+        request = new BodyCachingHttpServletRequestWrapper(request);
+
+        if (!helper.isOnApiList(request)) {
             proceed(filterChain, request, response);
         } else {
             try {
@@ -71,7 +74,7 @@ public class LoanCOBApiFilter extends OncePerRequestFilter {
                     proceed(filterChain, request, response);
                 } else {
                     try {
-                        List<Long> loanIds = helper.calculateRelevantLoanIds(request.getPathInfo());
+                        List<Long> loanIds = helper.calculateRelevantLoanIds(request);
                         if (!loanIds.isEmpty() && helper.isLoanBehind(loanIds)) {
                             helper.executeInlineCob(loanIds);
                         }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBBatchPreprocessor.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBBatchPreprocessor.java
deleted file mode 100644
index b4b39bedb..000000000
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBBatchPreprocessor.java
+++ /dev/null
@@ -1,74 +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.jobs.filter;
-
-import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned;
-
-import io.github.resilience4j.core.functions.Either;
-import java.util.List;
-import lombok.RequiredArgsConstructor;
-import org.apache.fineract.batch.domain.BatchRequest;
-import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
-import org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
-import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
-import org.springframework.context.annotation.Conditional;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.transaction.TransactionDefinition;
-import org.springframework.transaction.support.TransactionTemplate;
-
-@Component
-@RequiredArgsConstructor
-@Conditional(LoanCOBEnabledCondition.class)
-public class LoanCOBBatchPreprocessor implements BatchRequestPreprocessor {
-
-    private final LoanCOBFilterHelper helper;
-
-    private final PlatformTransactionManager transactionManager;
-
-    @Override
-    public Either<RuntimeException, BatchRequest> preprocess(BatchRequest batchRequest) {
-        TransactionTemplate tr = new TransactionTemplate(transactionManager);
-        tr.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
-        return tr.execute(status -> {
-            try {
-                String method = batchRequest.getMethod();
-                String relativeUrl = "/" + batchRequest.getRelativeUrl();
-                if (!isRelativeUrlVersioned(batchRequest.getRelativeUrl())) {
-                    // to support pre-versioned relative paths
-                    relativeUrl = "/v1/" + batchRequest.getRelativeUrl();
-                }
-                if (helper.isOnApiList(relativeUrl, method)) {
-                    boolean bypassUser = helper.isBypassUser();
-                    if (!bypassUser) {
-                        List<Long> result = helper.calculateRelevantLoanIds(relativeUrl);
-                        if (!result.isEmpty() && helper.isLoanBehind(result)) {
-                            helper.executeInlineCob(result);
-                        }
-                    }
-                }
-            } catch (LoanNotFoundException e) {
-                return Either.right(batchRequest);
-            } catch (RuntimeException e) {
-                return Either.left(e);
-            }
-            return Either.right(batchRequest);
-        });
-    }
-}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java
index 9b43a299c..c412a2dea 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBFilterHelper.java
@@ -18,7 +18,15 @@
  */
 package org.apache.fineract.infrastructure.jobs.filter;
 
+import static org.apache.fineract.batch.command.CommandStrategyUtils.isRelativeUrlVersioned;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.Lists;
+import jakarta.servlet.http.HttpServletRequest;
+import java.io.IOException;
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -28,6 +36,7 @@ import java.util.regex.Pattern;
 import lombok.RequiredArgsConstructor;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.batch.domain.BatchRequest;
 import org.apache.fineract.cob.conditions.LoanCOBEnabledCondition;
 import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
 import org.apache.fineract.cob.loan.RetrieveLoanIdService;
@@ -62,6 +71,7 @@ public class LoanCOBFilterHelper {
     private final RetrieveLoanIdService retrieveLoanIdService;
 
     private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
+    private final ObjectMapper objectMapper;
 
     private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE);
 
@@ -100,14 +110,51 @@ public class LoanCOBFilterHelper {
         return LOAN_PATH_PATTERN.matcher(pathInfo).matches() && pathInfo.contains("/v1/rescheduleloans/");
     }
 
-    public boolean isOnApiList(String pathInfo, String method) {
+    public boolean isOnApiList(HttpServletRequest request) throws IOException {
+        String pathInfo = request.getPathInfo();
+        String method = request.getMethod();
         if (StringUtils.isBlank(pathInfo)) {
             return false;
         }
+        if (isBatchApi(pathInfo)) {
+            return isBatchApiMatching(request);
+        } else {
+            return isApiMatching(method, pathInfo);
+        }
+    }
+
+    private boolean isBatchApiMatching(HttpServletRequest request) throws IOException {
+        for (BatchRequest batchRequest : getBatchRequests(request)) {
+            String method = batchRequest.getMethod();
+            String pathInfo = batchRequest.getRelativeUrl();
+            if (isApiMatching(method, pathInfo)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private List<BatchRequest> getBatchRequests(HttpServletRequest request) throws IOException {
+        List<BatchRequest> batchRequests = objectMapper.readValue(request.getInputStream(), new TypeReference<>() {});
+        for (BatchRequest batchRequest : batchRequests) {
+            String pathInfo = "/" + batchRequest.getRelativeUrl();
+            if (!isRelativeUrlVersioned(batchRequest.getRelativeUrl())) {
+                pathInfo = "/v1/" + batchRequest.getRelativeUrl();
+            }
+            batchRequest.setRelativeUrl(pathInfo);
+        }
+        return batchRequests;
+    }
+
+    private boolean isApiMatching(String method, String pathInfo) {
         return HTTP_METHODS.contains(HttpMethod.valueOf(method)) && !IGNORE_LOAN_PATH_PATTERN.matcher(pathInfo).find()
                 && URL_FUNCTION.test(pathInfo);
     }
 
+    private boolean isBatchApi(String pathInfo) {
+        return pathInfo.startsWith("/v1/batches");
+    }
+
     private boolean isGlim(String pathInfo) {
         return LOAN_GLIMACCOUNT_PATH_PATTERN.matcher(pathInfo).matches();
     }
@@ -138,8 +185,47 @@ public class LoanCOBFilterHelper {
         return CollectionUtils.isNotEmpty(loanIdAndLastClosedBusinessDates);
     }
 
-    public List<Long> calculateRelevantLoanIds(String pathInfo) {
+    public List<Long> calculateRelevantLoanIds(HttpServletRequest request) throws IOException {
+        String pathInfo = request.getPathInfo();
+        if (isBatchApi(pathInfo)) {
+            return getLoanIdsFromBatchApi(request);
+        } else {
+            return getLoanIdsFromApi(pathInfo);
+        }
+    }
+
+    private List<Long> getLoanIdsFromBatchApi(HttpServletRequest request) throws IOException {
+        List<Long> loanIds = new ArrayList<>();
+        for (BatchRequest batchRequest : getBatchRequests(request)) {
+            // check the URL for Loan related ID
+            String relativeUrl = batchRequest.getRelativeUrl();
+            if (!relativeUrl.contains("$.resourceId")) {
+                // if resourceId reference is used, we simply don't know the resourceId without executing the requests
+                // first, so skipping it
+                loanIds.addAll(getLoanIdsFromApi(relativeUrl));
+            }
+
+            // check the body for Loan ID
+            Long loanId = getTopLevelLoanIdFromBatchRequest(batchRequest);
+            if (loanId != null) {
+                loanIds.add(loanId);
+            }
+        }
+        return loanIds;
+    }
+
+    private Long getTopLevelLoanIdFromBatchRequest(BatchRequest batchRequest) throws JsonProcessingException {
+        String body = batchRequest.getBody();
+        if (StringUtils.isNotBlank(body)) {
+            JsonNode jsonNode = objectMapper.readTree(body);
+            if (jsonNode.has("loanId")) {
+                return jsonNode.get("loanId").asLong();
+            }
+        }
+        return null;
+    }
 
+    private List<Long> getLoanIdsFromApi(String pathInfo) {
         List<Long> loanIds = getLoanIdList(pathInfo);
         if (isLoanHardLocked(loanIds)) {
             throw new LoanIdsHardLockedException(loanIds.get(0));
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java
index 361310230..e4ad8e63d 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java
@@ -31,6 +31,7 @@ import static org.mockito.Mockito.verify;
 import com.sun.research.ws.wadl.HTTPMethods;
 import jakarta.servlet.FilterChain;
 import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.math.BigDecimal;
@@ -47,6 +48,7 @@ import org.apache.fineract.cob.service.LoanAccountLockService;
 import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
 import org.apache.fineract.infrastructure.core.config.FineractProperties;
 import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.http.BodyCachingHttpServletRequestWrapper;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
 import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
@@ -142,9 +144,10 @@ class LoanCOBApiFilterTest {
 
         given(request.getPathInfo()).willReturn("/v1/jobs/2/inline");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
 
         testObj.doFilterInternal(request, response, filterChain);
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -162,6 +165,7 @@ class LoanCOBApiFilterTest {
 
         given(request.getPathInfo()).willReturn("/v1/loans/invalid2LoanId/charges");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(context.authenticatedUser()).willReturn(appUser);
         given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
         given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000);
@@ -169,7 +173,7 @@ class LoanCOBApiFilterTest {
                 anyList())).willReturn(Collections.emptyList());
 
         testObj.doFilterInternal(request, response, filterChain);
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -181,11 +185,12 @@ class LoanCOBApiFilterTest {
 
         given(request.getPathInfo()).willReturn("/v1/jobs/2/inline");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(context.authenticatedUser()).willReturn(appUser);
         given(appUser.isBypassUser()).willReturn(true);
 
         testObj.doFilterInternal(request, response, filterChain);
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -203,6 +208,7 @@ class LoanCOBApiFilterTest {
 
         given(request.getPathInfo()).willReturn("/v1/loans/2/charges");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
         given(context.authenticatedUser()).willReturn(appUser);
         given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
@@ -211,7 +217,7 @@ class LoanCOBApiFilterTest {
                 anyList())).willReturn(Collections.emptyList());
 
         testObj.doFilterInternal(request, response, filterChain);
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -229,6 +235,7 @@ class LoanCOBApiFilterTest {
         String uuid = UUID.randomUUID().toString();
         given(request.getPathInfo()).willReturn("/v1/loans/external-id/" + uuid + "/charges");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
         given(context.authenticatedUser()).willReturn(appUser);
         given(loanRepository.findIdByExternalId(any())).willReturn(2L);
@@ -238,7 +245,7 @@ class LoanCOBApiFilterTest {
                 anyList())).willReturn(Collections.emptyList());
 
         testObj.doFilterInternal(request, response, filterChain);
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -256,6 +263,7 @@ class LoanCOBApiFilterTest {
         Long resourceId = 123L;
         given(request.getPathInfo()).willReturn("/v1/rescheduleloans/" + resourceId + "/charges");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
         given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
         given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000);
@@ -266,7 +274,7 @@ class LoanCOBApiFilterTest {
                 anyList())).willReturn(Collections.emptyList());
 
         testObj.doFilterInternal(request, response, filterChain);
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -288,6 +296,7 @@ class LoanCOBApiFilterTest {
         given(result.getLastClosedBusinessDate()).willReturn(businessDate.minusDays(2));
         given(request.getPathInfo()).willReturn("/v1/loans/2?command=approve");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
         given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
         given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000);
@@ -297,7 +306,7 @@ class LoanCOBApiFilterTest {
 
         testObj.doFilterInternal(request, response, filterChain);
         verify(inlineLoanCOBExecutorService, times(1)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -318,6 +327,7 @@ class LoanCOBApiFilterTest {
         given(result.getId()).willReturn(2L);
         given(request.getPathInfo()).willReturn("/v1/loans/2?command=approve");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false);
         given(fineractProperties.getQuery()).willReturn(fineractQueryProperties);
         given(fineractQueryProperties.getInClauseParameterSizeLimit()).willReturn(65000);
@@ -328,7 +338,7 @@ class LoanCOBApiFilterTest {
 
         testObj.doFilterInternal(request, response, filterChain);
         verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -340,12 +350,13 @@ class LoanCOBApiFilterTest {
 
         given(request.getPathInfo()).willReturn("/v1/loans");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
 
         given(context.authenticatedUser()).willReturn(appUser);
 
         testObj.doFilterInternal(request, response, filterChain);
         verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -357,12 +368,13 @@ class LoanCOBApiFilterTest {
 
         given(request.getPathInfo()).willReturn("/v1/loans/catch-up");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
 
         given(context.authenticatedUser()).willReturn(appUser);
 
         testObj.doFilterInternal(request, response, filterChain);
         verify(inlineLoanCOBExecutorService, times(0)).execute(Collections.singletonList(2L), "INLINE_LOAN_COB");
-        verify(filterChain, times(1)).doFilter(request, response);
+        verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), eq(response));
     }
 
     @Test
@@ -375,6 +387,7 @@ class LoanCOBApiFilterTest {
 
         given(request.getPathInfo()).willReturn("/v1/loans/2/charges");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(true);
         given(response.getWriter()).willReturn(writer);
         given(context.authenticatedUser()).willReturn(appUser);
@@ -396,6 +409,7 @@ class LoanCOBApiFilterTest {
 
         given(request.getPathInfo()).willReturn("/v1/loans/glimAccount/2");
         given(request.getMethod()).willReturn(HTTPMethods.POST.value());
+        given(request.getInputStream()).willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new byte[0]));
         given(glimAccountInfoRepository.findOneByIsAcceptingChildAndApplicationId(true, BigDecimal.valueOf(2))).willReturn(glimAccount);
         given(glimAccount.getChildLoan()).willReturn(Collections.singleton(loan));
         given(loan.getId()).willReturn(loanId);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index b04dd36eb..a406ca770 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -29,11 +29,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import io.restassured.builder.RequestSpecBuilder;
 import io.restassured.builder.ResponseSpecBuilder;
 import io.restassured.http.ContentType;
+import io.restassured.internal.RequestSpecificationImpl;
 import io.restassured.specification.RequestSpecification;
 import io.restassured.specification.ResponseSpecification;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -42,7 +44,10 @@ import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
 import lombok.ToString;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
 import org.apache.fineract.client.models.AdvancedPaymentData;
 import org.apache.fineract.client.models.AllowAttributeOverrides;
 import org.apache.fineract.client.models.BusinessDateRequest;
@@ -59,6 +64,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
 import org.apache.fineract.client.models.PostLoansRequest;
 import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.client.util.CallFailedRuntimeException;
+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.GlobalConfigurationHelper;
@@ -71,6 +77,8 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductHelper;
 import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
 import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
 import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
+import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
 import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
 import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
 import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
@@ -89,13 +97,18 @@ public abstract class BaseLoanIntegrationTest {
     protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
 
     protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200));
-    protected final RequestSpecification requestSpec = createRequestSpecification();
+
+    private final String fullAdminAuthKey = getFullAdminAuthKey();
+
+    protected final RequestSpecification requestSpec = createRequestSpecification(fullAdminAuthKey);
+    private final String nonByPassUserAuthKey = getNonByPassUserAuthKey(requestSpec, responseSpec);
 
     protected final AccountHelper accountHelper = new AccountHelper(requestSpec, responseSpec);
     protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
     protected final LoanProductHelper loanProductHelper = new LoanProductHelper();
     protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
     protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
+    protected final InlineLoanCOBHelper inlineLoanCOBHelper = new InlineLoanCOBHelper(requestSpec, responseSpec);
 
     protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
     protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
@@ -119,6 +132,16 @@ public abstract class BaseLoanIntegrationTest {
     protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
     protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
 
+    private String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) {
+        // creates the user
+        UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec);
+        return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(UserHelper.SIMPLE_USER_NAME, UserHelper.SIMPLE_USER_PASSWORD);
+    }
+
+    private String getFullAdminAuthKey() {
+        return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey();
+    }
+
     // Loan product with proper accounting setup
     protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() {
         return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))//
@@ -256,14 +279,14 @@ public abstract class BaseLoanIntegrationTest {
                 .amortizationType(amortizationType);
     }
 
-    private static RequestSpecification createRequestSpecification() {
-        RequestSpecification request = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
-        request.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
-        request.header("Fineract-Platform-TenantId", "default");
-        return request;
+    private RequestSpecification createRequestSpecification(String authKey) {
+        RequestSpecification requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        requestSpec.header("Authorization", "Basic " + authKey);
+        requestSpec.header("Fineract-Platform-TenantId", "default");
+        return requestSpec;
     }
 
-    protected static ResponseSpecification createResponseSpecification(Matcher<Integer> statusCodeMatcher) {
+    protected ResponseSpecification createResponseSpecification(Matcher<Integer> statusCodeMatcher) {
         return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build();
     }
 
@@ -317,6 +340,16 @@ public abstract class BaseLoanIntegrationTest {
         }
     }
 
+    protected void executeInlineCOB(Long loanId) {
+        inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
+    }
+
+    protected void verifyLastClosedBusinessDate(Long loanId, String lastClosedBusinessDate) {
+        GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
+        Assertions.assertNotNull(loanDetails.getLastClosedBusinessDate());
+        Assertions.assertEquals(lastClosedBusinessDate, loanDetails.getLastClosedBusinessDate().format(dateTimeFormatter));
+    }
+
     protected void disburseLoan(Long loanId, BigDecimal amount, String date) {
         loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate(date).dateFormat(DATETIME_PATTERN)
                 .transactionAmount(amount).locale("en"));
@@ -408,6 +441,16 @@ public abstract class BaseLoanIntegrationTest {
         }
     }
 
+    protected void runAsNonByPass(Runnable runnable) {
+        RequestSpecificationImpl requestSpecImpl = (RequestSpecificationImpl) requestSpec;
+        try {
+            requestSpecImpl.replaceHeader("Authorization", "Basic " + nonByPassUserAuthKey);
+            runnable.run();
+        } finally {
+            requestSpecImpl.replaceHeader("Authorization", "Basic " + fullAdminAuthKey);
+        }
+    }
+
     protected PostLoansRequest applyLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
             int numberOfRepayments) {
         return applyLoanRequest(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments, null);
@@ -508,6 +551,69 @@ public abstract class BaseLoanIntegrationTest {
         return new Installment(principalAmount, interestAmount, feeAmount, totalOutstandingAmount, completed, dueDate);
     }
 
+    protected BatchRequestBuilder batchRequest() {
+        return new BatchRequestBuilder(requestSpec, responseSpec);
+    }
+
+    @RequiredArgsConstructor
+    public static class BatchRequestBuilder {
+
+        private final RequestSpecification requestSpec;
+        private final ResponseSpecification responseSpec;
+        private List<BatchRequest> requests = new ArrayList<>();
+
+        public BatchRequestBuilder rescheduleLoan(Long requestId, Long loanId, String submittedOnDate, String rescheduleFromDate,
+                String adjustedDueDate) {
+            BatchRequest bRequest = new BatchRequest();
+            bRequest.setRequestId(requestId);
+            bRequest.setRelativeUrl("rescheduleloans");
+            bRequest.setMethod("POST");
+
+            bRequest.setBody("""
+                        {
+                            "loanId": %d,
+                            "rescheduleFromDate": "%s",
+                            "rescheduleReasonId": 1,
+                            "submittedOnDate": "%s",
+                            "rescheduleReasonComment": "",
+                            "adjustedDueDate": "%s",
+                            "graceOnPrincipal": "",
+                            "graceOnInterest": "",
+                            "extraTerms": "",
+                            "newInterestRate": "",
+                            "dateFormat": "%s",
+                            "locale": "en"
+                        }
+                    """.formatted(loanId, rescheduleFromDate, submittedOnDate, adjustedDueDate, DATETIME_PATTERN));
+
+            requests.add(bRequest);
+            return this;
+        }
+
+        public BatchRequestBuilder approveRescheduleLoan(Long requestId, Long rescheduleBatchRequestId, String approvedOnDate) {
+            BatchRequest bRequest = new BatchRequest();
+            bRequest.setRequestId(requestId);
+            bRequest.setRelativeUrl("rescheduleloans/$.resourceId?command=approve");
+            bRequest.setMethod("POST");
+            bRequest.setReference(rescheduleBatchRequestId);
+
+            bRequest.setBody("""
+                        {
+                            "approvedOnDate": "%s",
+                            "dateFormat": "%s",
+                            "locale": "en"
+                        }
+                    """.formatted(approvedOnDate, DATETIME_PATTERN));
+
+            requests.add(bRequest);
+            return this;
+        }
+
+        public List<BatchResponse> executeEnclosingTransaction() {
+            return BatchHelper.postBatchRequestsWithEnclosingTransaction(requestSpec, responseSpec, BatchHelper.toJsonString(requests));
+        }
+    }
+
     @ToString
     @AllArgsConstructor
     public static class Transaction {
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 24b59b233..fd2398012 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 java.lang.Integer.parseInt;
 import static org.apache.http.HttpStatus.SC_FORBIDDEN;
 import static org.hamcrest.Matchers.containsString;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -54,6 +55,7 @@ import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
 import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
 import org.apache.fineract.integrationtests.common.Utils;
 import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
+import org.apache.fineract.integrationtests.common.error.ErrorResponse;
 import org.apache.fineract.integrationtests.common.loans.LoanAccountLockHelper;
 import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
 import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
@@ -75,14 +77,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-/**
- * Test class for {@link org.apache.fineract.batch.command.CommandStrategyProvider}. This tests the response provided by
- * commandStrategy by injecting it with a {@code BatchRequest}.
- *
- * @author RishabhShukla
- * @see org.apache.fineract.integrationtests.common.BatchHelper
- * @see org.apache.fineract.batch.domain.BatchRequest
- */
 @ExtendWith(LoanTestLifecycleExtension.class)
 public class BatchApiTest {
 
@@ -1197,8 +1191,8 @@ public class BatchApiTest {
     }
 
     /**
-     * Test for the successful create client, creat, approve and get loan. A '200' status code is expected on successful
-     * responses.
+     * Test for the successful create client, create, approve and get loan. A '200' status code is expected on
+     * successful responses.
      *
      * @see org.apache.fineract.batch.command.internal.CreateClientCommandStrategy
      * @see org.apache.fineract.batch.command.internal.ApplyLoanCommandStrategy
@@ -2461,10 +2455,11 @@ public class BatchApiTest {
 
         final String jsonifiedRepaymentRequest = BatchHelper.toJsonString(List.of(br));
 
-        final List<BatchResponse> repaymentResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(requestSpec,
-                this.responseSpec, jsonifiedRepaymentRequest);
-
-        Assertions.assertEquals(HttpStatus.SC_CONFLICT, repaymentResponse.get(0).getStatusCode(), "Verify Status Code 409 for Locked Loan");
+        // verify HTTP 409
+        ResponseSpecification conflictResponseSpec = new ResponseSpecBuilder().expectStatusCode(409).build();
+        ErrorResponse errorResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransactionError(requestSpec, conflictResponseSpec,
+                jsonifiedRepaymentRequest);
+        assertEquals(409, parseInt(errorResponse.getHttpStatusCode()));
     }
 
     /**
@@ -2542,10 +2537,11 @@ public class BatchApiTest {
 
         final String jsonifiedRepaymentRequest = BatchHelper.toJsonString(List.of(br));
 
-        final List<BatchResponse> repaymentResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(requestSpec,
-                this.responseSpec, jsonifiedRepaymentRequest);
-
-        Assertions.assertEquals(HttpStatus.SC_CONFLICT, repaymentResponse.get(0).getStatusCode(), "Verify Status Code 409 for Locked Loan");
+        // verify HTTP 409
+        ResponseSpecification conflictResponseSpec = new ResponseSpecBuilder().expectStatusCode(409).build();
+        ErrorResponse errorResponse = BatchHelper.postBatchRequestsWithoutEnclosingTransactionError(requestSpec, conflictResponseSpec,
+                jsonifiedRepaymentRequest);
+        assertEquals(409, parseInt(errorResponse.getHttpStatusCode()));
     }
 
     @Test
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchLoanIntegrationTest.java
new file mode 100644
index 000000000..3fcc49dd7
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BatchLoanIntegrationTest.java
@@ -0,0 +1,101 @@
+/**
+ * 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;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.fineract.batch.domain.BatchResponse;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class BatchLoanIntegrationTest extends BaseLoanIntegrationTest {
+
+    @Test
+    public void test_InlineLoanCOB_ShouldExecute_WhenLoanIsBehind_And_RescheduleIsRequestedViaBatchApi() {
+        AtomicLong createdLoanId = new AtomicLong();
+
+        runAt("01 January 2023", () -> {
+            // Create Client
+            Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            int numberOfRepayments = 24;
+            int repaymentEvery = 1;
+
+            // Create Loan Product
+            PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
+                    .numberOfRepayments(numberOfRepayments) //
+                    .repaymentEvery(repaymentEvery) //
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
+
+            PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            // Apply and Approve Loan
+            double amount = 1250.0;
+
+            PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
+                    .repaymentEvery(repaymentEvery)//
+                    .loanTermFrequency(numberOfRepayments)//
+                    .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+                    .loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+            PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
+
+            PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+                    approveLoanRequest(amount, "01 January 2023"));
+
+            Long loanId = approvedLoanResult.getLoanId();
+
+            // disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
+
+            createdLoanId.set(loanId);
+        });
+
+        runAt("02 January 2023", () -> {
+            executeInlineCOB(createdLoanId.get());
+        });
+
+        runAt("05 January 2023", () -> {
+            runAsNonByPass(() -> {
+                long loanId = createdLoanId.get();
+
+                List<BatchResponse> responses = batchRequest() //
+                        .rescheduleLoan(1L, loanId, "01 January 2023", "01 February 2023", "01 March 2023") //
+                        .approveRescheduleLoan(2L, 1L, "01 January 2023") //
+                        .executeEnclosingTransaction(); //
+
+                Assertions.assertEquals(HttpStatus.SC_OK, responses.get(0).getStatusCode(),
+                        "Verify Status Code 200 for Create Reschedule Loan request");
+                Assertions.assertEquals(HttpStatus.SC_OK, responses.get(1).getStatusCode(),
+                        "Verify Status Code 200 for Approve Reschedule Loan request");
+
+                verifyLastClosedBusinessDate(loanId, "04 January 2023");
+            });
+        });
+    }
+}
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 ccdbc04fd..d744e74e6 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
@@ -35,6 +35,8 @@ import java.util.stream.Collectors;
 import org.apache.fineract.batch.command.internal.CreateTransactionLoanCommandStrategy;
 import org.apache.fineract.batch.domain.BatchRequest;
 import org.apache.fineract.batch.domain.BatchResponse;
+import org.apache.fineract.client.util.JSON;
+import org.apache.fineract.integrationtests.common.error.ErrorResponse;
 import org.apache.fineract.integrationtests.common.savings.SavingsTransactionData;
 import org.junit.jupiter.api.Assertions;
 import org.slf4j.Logger;
@@ -57,6 +59,8 @@ public final class BatchHelper {
     private static final String BATCH_API_WITHOUT_ENCLOSING_URL_EXT = BATCH_API_URL + "&enclosingTransaction=false";
     private static final SecureRandom secureRandom = new SecureRandom();
 
+    private static final Gson GSON = new JSON().getGson();
+
     private BatchHelper() {
 
     }
@@ -108,6 +112,14 @@ public final class BatchHelper {
         return BatchHelper.fromJsonString(response);
     }
 
+    public static ErrorResponse postBatchRequestsWithoutEnclosingTransactionError(final RequestSpecification requestSpec,
+            final ResponseSpecification responseSpec, final String jsonifiedBatchRequests) {
+        final String response = Utils.performServerPost(requestSpec, responseSpec, BATCH_API_WITHOUT_ENCLOSING_URL_EXT,
+                jsonifiedBatchRequests, null);
+        LOG.info("BatchHelper Response {}", response);
+        return GSON.fromJson(response, ErrorResponse.class);
+    }
+
     /**
      * Returns a list of BatchResponse with query parameter enclosing transaction set to true by posting the jsonified
      * BatchRequest to the server.
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/error/ErrorResponse.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/error/ErrorResponse.java
new file mode 100644
index 000000000..66e546508
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/error/ErrorResponse.java
@@ -0,0 +1,27 @@
+/**
+ * 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.error;
+
+import lombok.Data;
+
+@Data
+public class ErrorResponse {
+
+    private String httpStatusCode;
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/useradministration/users/UserHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/useradministration/users/UserHelper.java
index 23ea7177f..bdbfa402a 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/useradministration/users/UserHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/useradministration/users/UserHelper.java
@@ -43,8 +43,9 @@ public final class UserHelper {
     private static final String REPAYMENT_LOAN_PERMISSION = "REPAYMENT_LOAN";
     private static final String READ_LOAN_PERMISSION = "READ_LOAN";
 
-    private static boolean simpleUserCreated = false;
-    private static String simpleUsername;
+    public static final String SIMPLE_USER_NAME = Utils.uniqueRandomStringGenerator("NotificationUser", 4);
+    public static final String SIMPLE_USER_PASSWORD = "aA1qwerty56";
+    private static boolean SIMPLE_USER_CREATED = false;
 
     private UserHelper() {}
 
@@ -141,23 +142,22 @@ public final class UserHelper {
 
     public static RequestSpecification getSimpleUserWithoutBypassPermission(final RequestSpecification requestSpec,
             final ResponseSpecification responseSpec) {
-        String password = "aA1qwerty56";
-        if (!simpleUserCreated) {
+        String password = SIMPLE_USER_PASSWORD;
+        if (!SIMPLE_USER_CREATED) {
             GetOfficesResponse headOffice = OfficeHelper.getHeadOffice(requestSpec, responseSpec);
-            simpleUsername = Utils.uniqueRandomStringGenerator("NotificationUser", 4);
             String simpleRoleId = createSimpleRole(requestSpec, responseSpec);
-            PostUsersRequest createUserRequest = new PostUsersRequest().username(simpleUsername)
+            PostUsersRequest createUserRequest = new PostUsersRequest().username(SIMPLE_USER_NAME)
                     .firstname(Utils.randomStringGenerator("NotificationFN", 4)).lastname(Utils.randomStringGenerator("NotificationLN", 4))
                     .email("whatever@mifos.org").password(password).repeatPassword(password).sendPasswordToEmail(false)
                     .roles(List.of(Long.valueOf(simpleRoleId))).officeId(headOffice.getId());
 
             PostUsersResponse userCreationResponse = UserHelper.createUser(requestSpec, responseSpec, createUserRequest);
             Assertions.assertNotNull(userCreationResponse.getResourceId());
-            simpleUserCreated = true;
+            SIMPLE_USER_CREATED = true;
         }
         RequestSpecification responseRequestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
         responseRequestSpec.header("Authorization",
-                "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(simpleUsername, password));
+                "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(SIMPLE_USER_NAME, password));
         return responseRequestSpec;
     }
 
@@ -172,6 +172,10 @@ public final class UserHelper {
         HashMap<String, Boolean> permissionMap = new HashMap<>();
         permissionMap.put(REPAYMENT_LOAN_PERMISSION, true);
         permissionMap.put(READ_LOAN_PERMISSION, true);
+        permissionMap.put("READ_RESCHEDULELOAN", true);
+        permissionMap.put("CREATE_RESCHEDULELOAN", true);
+        permissionMap.put("REJECT_RESCHEDULELOAN", true);
+        permissionMap.put("APPROVE_RESCHEDULELOAN", true);
         RolesHelper.addPermissionsToRole(requestSpec, responseSpec, roleId, permissionMap);
     }
 }