You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ta...@apache.org on 2023/04/26 12:11:31 UTC

[fineract] branch develop updated: FINERACT-1724 - Loan CatchUp fix - [x] Update null loan cob date when hard lock - [x] Update hard lock to soft lock - [x] Ignore null loan cob date when catch up

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

taskain 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 88ab3dedb FINERACT-1724 - Loan CatchUp fix - [x] Update null loan cob date when hard lock - [x] Update hard lock to soft lock - [x] Ignore null loan cob date when catch up
88ab3dedb is described below

commit 88ab3dedbbf1418f0b48a693f4cba3f6f8f84436
Author: Janos Haber <ja...@finesolution.hu>
AuthorDate: Thu Apr 6 08:59:51 2023 +0200

    FINERACT-1724 - Loan CatchUp fix
    - [x] Update null loan cob date when hard lock
    - [x] Update hard lock to soft lock
    - [x] Ignore null loan cob date when catch up
---
 .../api/InternalLoanAccountLockApiResource.java    |   9 +-
 .../cob/api/LoanCOBCatchUpApiResource.java         |   2 +-
 .../cob/domain/LoanAccountLockRepository.java      |  25 +++
 .../fineract/cob/loan/ApplyLoanLockTasklet.java    |   6 +-
 .../LoanCatchUpSupport.java}                       |  18 +-
 .../fineract/cob/loan/LoanIdParameterTasklet.java  |   4 +-
 .../apache/fineract/cob/loan/LoanItemReader.java   |   5 +-
 .../fineract/cob/loan/LoanLockingService.java      |   2 +-
 .../fineract/cob/loan/LoanLockingServiceImpl.java  |  28 ++-
 .../apache/fineract/cob/loan/LockLoanTasklet.java  |   4 +-
 .../RetrieveAllNonClosedLoanIdServiceImpl.java     |  23 ++-
 .../fineract/cob/loan/RetrieveLoanIdService.java   |   4 +-
 .../service/AsyncLoanCOBExecutorServiceImpl.java   |   3 +-
 .../cob/service/LoanAccountLockService.java        |   2 +
 .../cob/service/LoanAccountLockServiceImpl.java    |  10 ++
 .../cob/service/LoanCOBCatchUpService.java         |   2 +
 .../cob/service/LoanCOBCatchUpServiceImpl.java     |   7 +
 .../loanaccount/domain/LoanRepository.java         |  11 ++
 .../loan/ApplyLoanLockTaskletStepDefinitions.java  |   3 +-
 .../cob/loan/FetchAndLockLoanStepDefinitions.java  |  18 +-
 .../cob/loan/LoanItemReaderStepDefinitions.java    |   3 +-
 .../LoanCatchUpIntegrationTest.java                | 191 +++++++++++++++++++++
 .../fineract/integrationtests/common/Utils.java    |  18 ++
 .../common/loans/LoanAccountLockHelper.java        |   7 +-
 .../common/loans/LoanCOBCatchUpHelper.java         |   7 +
 25 files changed, 366 insertions(+), 46 deletions(-)

diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
index 07447a2c3..ec29a0ff1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
@@ -29,6 +29,7 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.fineract.cob.domain.LoanAccountLock;
 import org.apache.fineract.cob.domain.LoanAccountLockRepository;
 import org.apache.fineract.cob.domain.LockOwner;
@@ -37,6 +38,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Component;
+import org.springframework.web.bind.annotation.RequestBody;
 
 @Profile("test")
 @Component
@@ -64,7 +66,7 @@ public class InternalLoanAccountLockApiResource implements InitializingBean {
     @Consumes({ MediaType.APPLICATION_JSON })
     @Produces({ MediaType.APPLICATION_JSON })
     public Response placeLockOnLoanAccount(@Context final UriInfo uriInfo, @PathParam("loanId") Long loanId,
-            @PathParam("lockOwner") String lockOwner) {
+            @PathParam("lockOwner") String lockOwner, @RequestBody(required = false) String error) {
         log.warn("------------------------------------------------------------");
         log.warn("                                                            ");
         log.warn("Placing lock on loan: {}", loanId);
@@ -73,7 +75,12 @@ public class InternalLoanAccountLockApiResource implements InitializingBean {
 
         LoanAccountLock loanAccountLock = new LoanAccountLock(loanId, LockOwner.valueOf(lockOwner),
                 ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE));
+
+        if (StringUtils.isNotBlank(error)) {
+            loanAccountLock.setError(error, error);
+        }
         loanAccountLockRepository.save(loanAccountLock);
         return Response.status(Response.Status.ACCEPTED).build();
     }
+
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java
index 5212c76c7..b76a601c9 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanCOBCatchUpApiResource.java
@@ -57,7 +57,6 @@ public class LoanCOBCatchUpApiResource {
             @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanCOBCatchUpApiResourceSwagger.GetOldestCOBProcessedLoanResponse.class))) })
     public String getOldestCOBProcessedLoan() {
         OldestCOBProcessedLoanDTO response = loanCOBCatchUpService.getOldestCOBProcessedLoan();
-
         return oldestCOBProcessedLoanSerializeService.serialize(response);
     }
 
@@ -73,6 +72,7 @@ public class LoanCOBCatchUpApiResource {
         if (loanCOBCatchUpService.isCatchUpRunning().isCatchUpRunning()) {
             return Response.status(Response.Status.BAD_REQUEST).build();
         }
+        loanCOBCatchUpService.unlockHardLockedLoans();
         OldestCOBProcessedLoanDTO oldestCOBProcessedLoan = loanCOBCatchUpService.getOldestCOBProcessedLoan();
         if (oldestCOBProcessedLoan.getCobProcessedDate().equals(oldestCOBProcessedLoan.getCobBusinessDate())) {
             return Response.status(Response.Status.OK).build();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java
index 6ba104ffb..d2dc59e05 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLockRepository.java
@@ -22,6 +22,8 @@ import java.util.List;
 import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
 
 public interface LoanAccountLockRepository extends JpaRepository<LoanAccountLock, Long>, JpaSpecificationExecutor<LoanAccountLock> {
 
@@ -32,4 +34,27 @@ public interface LoanAccountLockRepository extends JpaRepository<LoanAccountLock
     List<LoanAccountLock> findAllByLoanIdIn(List<Long> loanIds);
 
     boolean existsByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);
+
+    @Query(value = """
+                                                 update m_loan set last_closed_business_date = (select lck.lock_placed_on_cob_business_date - 1
+                                                 from m_loan_account_locks lck
+                                                 where lck.loan_id = id
+                                                   and lck.lock_placed_on_cob_business_date is not null
+                                                   and lck.error is not null
+                                                   and lck.lock_owner in ('LOAN_COB_CHUNK_PROCESSING','LOAN_INLINE_COB_PROCESSING'))
+            where last_closed_business_date is null and exists  (select lck.loan_id
+                          from m_loan_account_locks lck  where lck.loan_id = id
+                            and lck.lock_placed_on_cob_business_date is not null and lck.error is not null
+                            and lck.lock_owner in ('LOAN_COB_CHUNK_PROCESSING','LOAN_INLINE_COB_PROCESSING'))""", nativeQuery = true)
+    @Modifying(flushAutomatically = true)
+    void updateLoanFromAccountLocks();
+
+    @Query("""
+            update LoanAccountLock lck set
+            lck.error = null, lck.lockOwner=org.apache.fineract.cob.domain.LockOwner.LOAN_COB_PARTITIONING
+            where lck.lockPlacedOnCobBusinessDate is not null and lck.error is not null and
+            lck.lockOwner in (org.apache.fineract.cob.domain.LockOwner.LOAN_COB_CHUNK_PROCESSING,org.apache.fineract.cob.domain.LockOwner.LOAN_INLINE_COB_PROCESSING)
+            """)
+    @Modifying(flushAutomatically = true)
+    void updateToSoftLockByOwner();
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java
index 2f949a27c..6efb01bb2 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java
@@ -41,7 +41,7 @@ import org.springframework.batch.repeat.RepeatStatus;
 
 @Slf4j
 @RequiredArgsConstructor
-public class ApplyLoanLockTasklet implements Tasklet {
+public class ApplyLoanLockTasklet implements Tasklet, LoanCatchUpSupport {
 
     private final FineractProperties fineractProperties;
     private final LoanLockingService loanLockingService;
@@ -57,8 +57,8 @@ public class ApplyLoanLockTasklet implements Tasklet {
                 || (loanCOBParameter.getMinLoanId().equals(0L) && loanCOBParameter.getMaxLoanId().equals(0L))) {
             loanIds = Collections.emptyList();
         } else {
-            loanIds = new ArrayList<>(
-                    retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter));
+            loanIds = new ArrayList<>(retrieveLoanIdService
+                    .retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, isCatchUp(contribution)));
         }
         List<List<Long>> loanIdPartitions = Lists.partition(loanIds, getInClauseParameterSizeLimit());
         List<LoanAccountLock> accountLocks = new ArrayList<>();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCatchUpSupport.java
similarity index 61%
copy from fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java
copy to fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCatchUpSupport.java
index 12abcc961..ce382ca31 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCatchUpSupport.java
@@ -16,16 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.service;
+package org.apache.fineract.cob.loan;
 
-import org.apache.fineract.cob.data.IsCatchUpRunningDTO;
-import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
+import org.springframework.batch.core.StepContribution;
+import org.springframework.batch.core.StepExecution;
 
-public interface LoanCOBCatchUpService {
+public interface LoanCatchUpSupport {
 
-    OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan();
+    default boolean isCatchUp(StepContribution contribution) {
+        return isCatchUp(contribution.getStepExecution());
+    }
 
-    void executeLoanCOBCatchUp();
-
-    IsCatchUpRunningDTO isCatchUpRunning();
+    default boolean isCatchUp(StepExecution execution) {
+        return "true".equalsIgnoreCase(execution.getExecutionContext().getString(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME, "false"));
+    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanIdParameterTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanIdParameterTasklet.java
index 78e73253e..75b6ed6f2 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanIdParameterTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanIdParameterTasklet.java
@@ -30,7 +30,7 @@ import org.springframework.batch.repeat.RepeatStatus;
 
 @Slf4j
 @RequiredArgsConstructor
-public class LoanIdParameterTasklet implements Tasklet {
+public class LoanIdParameterTasklet implements Tasklet, LoanCatchUpSupport {
 
     private final RetrieveLoanIdService retrieveLoanIdService;
 
@@ -40,7 +40,7 @@ public class LoanIdParameterTasklet implements Tasklet {
                 .get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME);
         LocalDate businessDate = LocalDate.parse(Objects.requireNonNull(businessDateParameter));
         LoanCOBParameter minAndMaxLoanId = retrieveLoanIdService.retrieveMinAndMaxLoanIdsNDaysBehind(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND,
-                businessDate);
+                businessDate, isCatchUp(contribution));
         if (Objects.isNull(minAndMaxLoanId)
                 || (Objects.isNull(minAndMaxLoanId.getMinLoanId()) && Objects.isNull(minAndMaxLoanId.getMaxLoanId()))) {
             contribution.getStepExecution().getJobExecution().getExecutionContext().put(LoanCOBConstant.LOAN_COB_PARAMETER,
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java
index ad7c87374..def3ab969 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java
@@ -29,7 +29,7 @@ import org.springframework.batch.core.StepExecution;
 import org.springframework.batch.core.annotation.BeforeStep;
 import org.springframework.batch.item.ExecutionContext;
 
-public class LoanItemReader extends AbstractLoanItemReader {
+public class LoanItemReader extends AbstractLoanItemReader implements LoanCatchUpSupport {
 
     private final RetrieveLoanIdService retrieveLoanIdService;
 
@@ -49,7 +49,8 @@ public class LoanItemReader extends AbstractLoanItemReader {
                 || (loanCOBParameter.getMinLoanId().equals(0L) && loanCOBParameter.getMaxLoanId().equals(0L))) {
             loanIds = Collections.emptyList();
         } else {
-            loanIds = retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter);
+            loanIds = retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter,
+                    isCatchUp(stepExecution));
         }
         setRemainingData(new ArrayList<>(loanIds));
     }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java
index 3a7eadc69..f049e1351 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingService.java
@@ -26,7 +26,7 @@ import org.apache.fineract.cob.domain.LockOwner;
 
 public interface LoanLockingService {
 
-    void applySoftLock(LocalDate lastClosedBusinessDate, LoanCOBParameter loanCOBParameter);
+    void applySoftLock(LocalDate lastClosedBusinessDate, LoanCOBParameter loanCOBParameter, boolean isCatchUp);
 
     void upgradeLock(List<Long> accountsToLock, LockOwner lockOwner);
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java
index bf4c76a76..3c44d2db6 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanLockingServiceImpl.java
@@ -36,22 +36,32 @@ import org.springframework.jdbc.core.JdbcTemplate;
 @Slf4j
 public class LoanLockingServiceImpl implements LoanLockingService {
 
+    private static final String NORMAL_LOAN_INSERT = """
+                INSERT INTO m_loan_account_locks (loan_id, version, lock_owner, lock_placed_on, lock_placed_on_cob_business_date)
+                SELECT loan.id, ?, ?, ?, ? FROM m_loan loan
+                    WHERE loan.id NOT IN (SELECT loan_id FROM m_loan_account_locks)
+                    AND loan.id BETWEEN ? AND ?
+                    AND loan.loan_status_id IN (100,200,300,303,304)
+                    AND (? = loan.last_closed_business_date OR loan.last_closed_business_date IS NULL)
+            """;
+    private static final String CATCH_UP_LOAN_INSERT = """
+                INSERT INTO m_loan_account_locks (loan_id, version, lock_owner, lock_placed_on, lock_placed_on_cob_business_date)
+                SELECT loan.id, ?, ?, ?, ? FROM m_loan loan
+                    WHERE loan.id NOT IN (SELECT loan_id FROM m_loan_account_locks)
+                    AND loan.id BETWEEN ? AND ?
+                    AND loan.loan_status_id IN (100,200,300,303,304)
+                    AND (? = loan.last_closed_business_date)
+            """;
+
     private final JdbcTemplate jdbcTemplate;
     private final FineractProperties fineractProperties;
     private final LoanAccountLockRepository loanAccountLockRepository;
 
     @Override
-    public void applySoftLock(LocalDate lastClosedBusinessDate, LoanCOBParameter loanCOBParameter) {
+    public void applySoftLock(LocalDate lastClosedBusinessDate, LoanCOBParameter loanCOBParameter, boolean isCatchUp) {
 
         LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
-        jdbcTemplate.update("""
-                    INSERT INTO m_loan_account_locks (loan_id, version, lock_owner, lock_placed_on, lock_placed_on_cob_business_date)
-                    SELECT loan.id, ?, ?, ?, ? FROM m_loan loan
-                        WHERE loan.id NOT IN (SELECT loan_id FROM m_loan_account_locks)
-                        AND loan.id BETWEEN ? AND ?
-                        AND loan.loan_status_id IN (100,200,300,303,304)
-                        AND (? = loan.last_closed_business_date OR loan.last_closed_business_date IS NULL)
-                """, ps -> {
+        jdbcTemplate.update(isCatchUp ? CATCH_UP_LOAN_INSERT : NORMAL_LOAN_INSERT, ps -> {
             ps.setLong(1, 1);
             ps.setString(2, LockOwner.LOAN_COB_PARTITIONING.name());
             ps.setObject(3, DateUtils.getOffsetDateTimeOfTenant());
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LockLoanTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LockLoanTasklet.java
index f313998aa..e0768c488 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LockLoanTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LockLoanTasklet.java
@@ -31,7 +31,7 @@ import org.springframework.batch.repeat.RepeatStatus;
 
 @Slf4j
 @RequiredArgsConstructor
-public class LockLoanTasklet implements Tasklet {
+public class LockLoanTasklet implements Tasklet, LoanCatchUpSupport {
 
     private final LoanLockingService loanLockingService;
 
@@ -47,7 +47,7 @@ public class LockLoanTasklet implements Tasklet {
                 || (Objects.isNull(loanCOBParameter.getMinLoanId()) && Objects.isNull(loanCOBParameter.getMaxLoanId()))) {
             loanCOBParameter = new LoanCOBParameter(0L, 0L);
         }
-        loanLockingService.applySoftLock(lastClosedBusinessDate, loanCOBParameter);
+        loanLockingService.applySoftLock(lastClosedBusinessDate, loanCOBParameter, isCatchUp(contribution));
 
         return RepeatStatus.FINISHED;
     }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
index 258512ca3..ecc1f6ebd 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
@@ -34,8 +34,12 @@ public class RetrieveAllNonClosedLoanIdServiceImpl implements RetrieveLoanIdServ
     private final LoanRepository loanRepository;
 
     @Override
-    public LoanCOBParameter retrieveMinAndMaxLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate) {
-        return loanRepository.findMinAndMaxNonClosedLoanIdsByLastClosedBusinessDate(businessDate.minusDays(numberOfDays));
+    public LoanCOBParameter retrieveMinAndMaxLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate, boolean isCatchUp) {
+        if (isCatchUp) {
+            return loanRepository.findMinAndMaxNonClosedLoanIdsByLastClosedBusinessDateNotNull(businessDate.minusDays(numberOfDays));
+        } else {
+            return loanRepository.findMinAndMaxNonClosedLoanIdsByLastClosedBusinessDate(businessDate.minusDays(numberOfDays));
+        }
     }
 
     @Override
@@ -49,10 +53,17 @@ public class RetrieveAllNonClosedLoanIdServiceImpl implements RetrieveLoanIdServ
     }
 
     @Override
-    public List<Long> retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(LoanCOBParameter loanCOBParameter) {
-        return loanRepository.findAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter.getMinLoanId(),
-                loanCOBParameter.getMaxLoanId(),
-                ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE).minusDays(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND));
+    public List<Long> retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(LoanCOBParameter loanCOBParameter,
+            boolean isCatchUp) {
+        if (isCatchUp) {
+            return loanRepository.findAllNonClosedLoansByLastClosedBusinessDateNotNullAndMinAndMaxLoanId(loanCOBParameter.getMinLoanId(),
+                    loanCOBParameter.getMaxLoanId(), ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)
+                            .minusDays(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND));
+        } else {
+            return loanRepository.findAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter.getMinLoanId(),
+                    loanCOBParameter.getMaxLoanId(), ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE)
+                            .minusDays(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND));
+        }
     }
 
     @Override
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java
index 0063909cc..681e0ab26 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java
@@ -27,13 +27,13 @@ import org.springframework.data.repository.query.Param;
 
 public interface RetrieveLoanIdService {
 
-    LoanCOBParameter retrieveMinAndMaxLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate);
+    LoanCOBParameter retrieveMinAndMaxLoanIdsNDaysBehind(Long numberOfDays, LocalDate businessDate, boolean isCatchUp);
 
     List<LoanIdAndLastClosedBusinessDate> retrieveLoanIdsBehindDateOrNull(LocalDate businessDate, List<Long> loanIds);
 
     List<LoanIdAndLastClosedBusinessDate> retrieveLoanIdsOldestCobProcessed(LocalDate businessDate);
 
-    List<Long> retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(LoanCOBParameter loanCOBParameter);
+    List<Long> retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(LoanCOBParameter loanCOBParameter, boolean isCatchUp);
 
     List<LoanIdAndExternalIdAndAccountNo> findAllStayedLockedByCobBusinessDate(@Param("cobBusinessDate") LocalDate cobBusinessDate);
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
index 72d09a774..6f3fc7fba 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
@@ -37,7 +37,6 @@ import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
 import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetailRepository;
 import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
 import org.apache.fineract.infrastructure.jobs.service.JobStarter;
-import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
 import org.springframework.batch.core.Job;
 import org.springframework.batch.core.JobParametersInvalidException;
 import org.springframework.batch.core.configuration.JobLocator;
@@ -52,7 +51,6 @@ import org.springframework.stereotype.Service;
 @RequiredArgsConstructor
 public class AsyncLoanCOBExecutorServiceImpl implements AsyncLoanCOBExecutorService {
 
-    private final LoanRepository loanRepository;
     private final JobLocator jobLocator;
     private final ScheduledJobDetailRepository scheduledJobDetailRepository;
     private final JobStarter jobStarter;
@@ -67,6 +65,7 @@ public class AsyncLoanCOBExecutorServiceImpl implements AsyncLoanCOBExecutorServ
             LocalDate cobBusinessDate = ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.COB_DATE);
             List<LoanIdAndLastClosedBusinessDate> loanIdAndLastClosedBusinessDate = retrieveLoanIdService
                     .retrieveLoanIdsOldestCobProcessed(cobBusinessDate);
+
             LocalDate oldestCOBProcessedDate = !loanIdAndLastClosedBusinessDate.isEmpty()
                     ? loanIdAndLastClosedBusinessDate.get(0).getLastClosedBusinessDate()
                     : cobBusinessDate;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java
index 5fd6eaeee..98760cc80 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockService.java
@@ -28,4 +28,6 @@ public interface LoanAccountLockService {
     boolean isLoanHardLocked(Long loanId);
 
     boolean isLoanSoftLocked(Long loanId);
+
+    void updateCobAndRemoveLocks();
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java
index 29423638b..673eef2d5 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanAccountLockServiceImpl.java
@@ -27,6 +27,8 @@ import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
 
 @Service
 @RequiredArgsConstructor
@@ -51,4 +53,12 @@ public class LoanAccountLockServiceImpl implements LoanAccountLockService {
     public boolean isLoanSoftLocked(Long loanId) {
         return loanAccountLockRepository.existsByLoanIdAndLockOwner(loanId, LockOwner.LOAN_COB_PARTITIONING);
     }
+
+    @Override
+    @Transactional(propagation = Propagation.REQUIRES_NEW)
+    public void updateCobAndRemoveLocks() {
+        loanAccountLockRepository.updateLoanFromAccountLocks();
+        loanAccountLockRepository.updateToSoftLockByOwner();
+    }
+
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java
index 12abcc961..fac0c1bc7 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpService.java
@@ -23,6 +23,8 @@ import org.apache.fineract.cob.data.OldestCOBProcessedLoanDTO;
 
 public interface LoanCOBCatchUpService {
 
+    void unlockHardLockedLoans();
+
     OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan();
 
     void executeLoanCOBCatchUp();
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java
index e5c9231de..2cc77d04f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/LoanCOBCatchUpServiceImpl.java
@@ -45,6 +45,13 @@ public class LoanCOBCatchUpServiceImpl implements LoanCOBCatchUpService {
     private final JobExplorer jobExplorer;
     private final RetrieveLoanIdService retrieveLoanIdService;
 
+    private final LoanAccountLockService accountLockService;
+
+    @Override
+    public void unlockHardLockedLoans() {
+        accountLockService.updateCobAndRemoveLocks();
+    }
+
     @Override
     public OldestCOBProcessedLoanDTO getOldestCOBProcessedLoan() {
         List<LoanIdAndLastClosedBusinessDate> loanIdAndLastClosedBusinessDate = retrieveLoanIdService
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java
index de3de3bef..09f502a32 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java
@@ -78,6 +78,9 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
     String FIND_MIN_AND_MAX_NON_CLOSED_LOAN_IDS_BY_LAST_CLOSED_BUSINESS_DATE = "select new org.apache.fineract.cob.data.LoanCOBParameter(min(loan.id), max(loan.id)) from Loan loan where loan.loanStatus in (100,200,300,303,304) "
             + "and (:businessDate = loan.lastClosedBusinessDate or loan.lastClosedBusinessDate is NULL)";
 
+    String FIND_MIN_AND_MAX_NON_CLOSED_AND_NON_NULL_LOAN_IDS_BY_LAST_CLOSED_BUSINESS_DATE = "select new org.apache.fineract.cob.data.LoanCOBParameter(min(loan.id), max(loan.id)) from Loan loan where loan.loanStatus in (100,200,300,303,304) "
+            + "and :businessDate = loan.lastClosedBusinessDate";
+
     String FIND_NON_CLOSED_LOAN_THAT_BELONGS_TO_CLIENT = "select loan from Loan loan where loan.id = :loanId and loan.loanStatus = 300 and loan.client.id = :clientId";
 
     String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where loan.accountNumber = :accountNumber";
@@ -93,6 +96,7 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
 
     String FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE_AND_MIN_AND_MAX_LOAN_ID = "select loan.id from Loan loan where loan.id BETWEEN :minLoanId and :maxLoanId and loan.loanStatus in (100,200,300,303,304) and (:cobBusinessDate = loan.lastClosedBusinessDate or loan.lastClosedBusinessDate is NULL)";
 
+    String FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE_NOT_NULL_AND_MIN_AND_MAX_LOAN_ID = "select loan.id from Loan loan where loan.id BETWEEN :minLoanId and :maxLoanId and loan.loanStatus in (100,200,300,303,304) and :cobBusinessDate = loan.lastClosedBusinessDate";
     String FIND_ALL_NON_CLOSED_LOANS_BEHIND_BY_LOAN_IDS = "select loan.id, loan.lastClosedBusinessDate from Loan loan where loan.id IN :loanIds and loan.loanStatus in (100,200,300,303,304) and loan.lastClosedBusinessDate < :cobBusinessDate";
 
     String FIND_ALL_STAYED_LOCKED_BY_COB_BUSINESS_DATE = "select loan.id, loan.externalId, loan.accountNumber from LoanAccountLock lock left join Loan loan on lock.loanId = loan.id where lock.lockPlacedOnCobBusinessDate = :cobBusinessDate";
@@ -198,6 +202,9 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
     @Query(FIND_MIN_AND_MAX_NON_CLOSED_LOAN_IDS_BY_LAST_CLOSED_BUSINESS_DATE)
     LoanCOBParameter findMinAndMaxNonClosedLoanIdsByLastClosedBusinessDate(@Param("businessDate") LocalDate businessDate);
 
+    @Query(FIND_MIN_AND_MAX_NON_CLOSED_AND_NON_NULL_LOAN_IDS_BY_LAST_CLOSED_BUSINESS_DATE)
+    LoanCOBParameter findMinAndMaxNonClosedLoanIdsByLastClosedBusinessDateNotNull(@Param("businessDate") LocalDate businessDate);
+
     @Query(FIND_ALL_NON_CLOSED_LOANS_BEHIND_BY_LOAN_IDS)
     List<LoanIdAndLastClosedBusinessDate> findAllNonClosedLoansBehindByLoanIds(@Param("cobBusinessDate") LocalDate cobBusinessDate,
             @Param("loanIds") List<Long> loanIds);
@@ -210,6 +217,10 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
     List<Long> findAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(@Param("minLoanId") Long minLoanId,
             @Param("maxLoanId") Long maxLoanId, @Param("cobBusinessDate") LocalDate cobBusinessDate);
 
+    @Query(FIND_ALL_NON_CLOSED_LOANS_BY_LAST_CLOSED_BUSINESS_DATE_NOT_NULL_AND_MIN_AND_MAX_LOAN_ID)
+    List<Long> findAllNonClosedLoansByLastClosedBusinessDateNotNullAndMinAndMaxLoanId(@Param("minLoanId") Long minLoanId,
+            @Param("maxLoanId") Long maxLoanId, @Param("cobBusinessDate") LocalDate cobBusinessDate);
+
     @Query(FIND_OLDEST_COB_PROCESSED_LOAN)
     List<LoanIdAndLastClosedBusinessDate> findOldestCOBProcessedLoan(@Param("cobBusinessDate") LocalDate cobBusinessDate);
 
diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java
index 37851a9da..fe1060036 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java
@@ -66,7 +66,8 @@ public class ApplyLoanLockTaskletStepDefinitions implements En {
             ExecutionContext executionContext = new ExecutionContext();
             LoanCOBParameter loanCOBParameter = new LoanCOBParameter(1L, 4L);
             executionContext.put(LoanCOBConstant.LOAN_COB_PARAMETER, loanCOBParameter);
-            lenient().when(retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter))
+            lenient().when(
+                    retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false))
                     .thenReturn(List.of(1L, 2L, 3L, 4L));
             stepExecution.setExecutionContext(executionContext);
             stepContribution = new StepContribution(stepExecution);
diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/FetchAndLockLoanStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/FetchAndLockLoanStepDefinitions.java
index bbf77a9be..147cb883d 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/FetchAndLockLoanStepDefinitions.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/FetchAndLockLoanStepDefinitions.java
@@ -53,6 +53,8 @@ public class FetchAndLockLoanStepDefinitions implements En {
     StepContribution contribution;
     ArgumentCaptor<LocalDate> dateValueCaptor = ArgumentCaptor.forClass(LocalDate.class);
     ArgumentCaptor<LoanCOBParameter> loanCOBParameterValueCaptor = ArgumentCaptor.forClass(LoanCOBParameter.class);
+
+    ArgumentCaptor<Boolean> isCatchUpParameterCaptor = ArgumentCaptor.forClass(Boolean.class);
     private LockLoanTasklet lockLoanTasklet;
     private String action;
     private RepeatStatus result;
@@ -115,44 +117,52 @@ public class FetchAndLockLoanStepDefinitions implements En {
             if ("empty steps".equals(action)) {
                 assertEquals(RepeatStatus.FINISHED, result);
             } else if ("good".equals(action)) {
-                verify(loanLockingService).applySoftLock(dateValueCaptor.capture(), loanCOBParameterValueCaptor.capture());
+                verify(loanLockingService).applySoftLock(dateValueCaptor.capture(), loanCOBParameterValueCaptor.capture(),
+                        isCatchUpParameterCaptor.capture());
                 assertEquals(LocalDate.now(ZoneId.systemDefault()).minusDays(1), dateValueCaptor.getValue());
                 assertEquals(1L, loanCOBParameterValueCaptor.getValue().getMinLoanId());
                 assertEquals(3L, loanCOBParameterValueCaptor.getValue().getMaxLoanId());
                 assertEquals(RepeatStatus.FINISHED, result);
+                assertEquals(false, isCatchUpParameterCaptor.getValue());
                 LoanCOBParameter loanCOBParameter = (LoanCOBParameter) contribution.getStepExecution().getJobExecution()
                         .getExecutionContext().get(LoanCOBConstant.LOAN_COB_PARAMETER);
                 assertEquals(2, loanCOBParameter.getMaxLoanId() - loanCOBParameter.getMinLoanId());
                 assertEquals(1L, loanCOBParameter.getMinLoanId());
                 assertEquals(3L, loanCOBParameter.getMaxLoanId());
             } else if ("soft lock".equals(action)) {
-                verify(loanLockingService).applySoftLock(dateValueCaptor.capture(), loanCOBParameterValueCaptor.capture());
+                verify(loanLockingService).applySoftLock(dateValueCaptor.capture(), loanCOBParameterValueCaptor.capture(),
+                        isCatchUpParameterCaptor.capture());
                 assertEquals(LocalDate.now(ZoneId.systemDefault()).minusDays(1), dateValueCaptor.getValue());
                 assertEquals(1L, loanCOBParameterValueCaptor.getValue().getMinLoanId());
                 assertEquals(3L, loanCOBParameterValueCaptor.getValue().getMaxLoanId());
                 assertEquals(RepeatStatus.FINISHED, result);
+                assertEquals(false, isCatchUpParameterCaptor.getValue());
                 LoanCOBParameter loanCOBParameter = (LoanCOBParameter) contribution.getStepExecution().getJobExecution()
                         .getExecutionContext().get(LoanCOBConstant.LOAN_COB_PARAMETER);
                 assertEquals(2, loanCOBParameter.getMaxLoanId() - loanCOBParameter.getMinLoanId());
                 assertEquals(1L, loanCOBParameter.getMinLoanId());
                 assertEquals(3L, loanCOBParameter.getMaxLoanId());
             } else if ("inline cob".equals(action)) {
-                verify(loanLockingService).applySoftLock(dateValueCaptor.capture(), loanCOBParameterValueCaptor.capture());
+                verify(loanLockingService).applySoftLock(dateValueCaptor.capture(), loanCOBParameterValueCaptor.capture(),
+                        isCatchUpParameterCaptor.capture());
                 assertEquals(LocalDate.now(ZoneId.systemDefault()).minusDays(1), dateValueCaptor.getValue());
                 assertEquals(1L, loanCOBParameterValueCaptor.getValue().getMinLoanId());
                 assertEquals(3L, loanCOBParameterValueCaptor.getValue().getMaxLoanId());
                 assertEquals(RepeatStatus.FINISHED, result);
+                assertEquals(false, isCatchUpParameterCaptor.getValue());
                 LoanCOBParameter loanCOBParameter = (LoanCOBParameter) contribution.getStepExecution().getJobExecution()
                         .getExecutionContext().get(LoanCOBConstant.LOAN_COB_PARAMETER);
                 assertEquals(2, loanCOBParameter.getMaxLoanId() - loanCOBParameter.getMinLoanId());
                 assertEquals(1L, loanCOBParameter.getMinLoanId());
                 assertEquals(3L, loanCOBParameter.getMaxLoanId());
             } else if ("chunk processing".equals(action)) {
-                verify(loanLockingService).applySoftLock(dateValueCaptor.capture(), loanCOBParameterValueCaptor.capture());
+                verify(loanLockingService).applySoftLock(dateValueCaptor.capture(), loanCOBParameterValueCaptor.capture(),
+                        isCatchUpParameterCaptor.capture());
                 assertEquals(LocalDate.now(ZoneId.systemDefault()).minusDays(1), dateValueCaptor.getValue());
                 assertEquals(1L, loanCOBParameterValueCaptor.getValue().getMinLoanId());
                 assertEquals(3L, loanCOBParameterValueCaptor.getValue().getMaxLoanId());
                 assertEquals(RepeatStatus.FINISHED, result);
+                assertEquals(false, isCatchUpParameterCaptor.getValue());
                 LoanCOBParameter loanCOBParameter = (LoanCOBParameter) contribution.getStepExecution().getJobExecution()
                         .getExecutionContext().get(LoanCOBConstant.LOAN_COB_PARAMETER);
                 assertEquals(2, loanCOBParameter.getMaxLoanId() - loanCOBParameter.getMinLoanId());
diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java
index 7c0528a0f..2792ef9e9 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java
@@ -74,7 +74,8 @@ public class LoanItemReaderStepDefinitions implements En {
             stepExecutionContext.put(LoanCOBConstant.LOAN_COB_PARAMETER, loanCOBParameter);
             stepExecution.setExecutionContext(stepExecutionContext);
 
-            lenient().when(this.retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter))
+            lenient().when(
+                    this.retrieveLoanIdService.retrieveAllNonClosedLoansByLastClosedBusinessDateAndMinAndMaxLoanId(loanCOBParameter, false))
                     .thenReturn(splitAccounts);
 
             HashMap<BusinessDateType, LocalDate> businessDates = new HashMap<>();
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCatchUpIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCatchUpIntegrationTest.java
new file mode 100644
index 000000000..0bb766ece
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCatchUpIntegrationTest.java
@@ -0,0 +1,191 @@
+/**
+ * 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 io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.path.json.JsonPath;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+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.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.loans.LoanAccountLockHelper;
+import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanCOBCatchUpHelper;
+import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
+import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(LoanTestLifecycleExtension.class)
+public class LoanCatchUpIntegrationTest {
+
+    private static final String REPAYMENT_LOAN_PERMISSION = "REPAYMENT_LOAN";
+    private static final String READ_LOAN_PERMISSION = "READ_LOAN";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private LoanCOBCatchUpHelper loanCOBCatchUpHelper;
+    private LoanTransactionHelper loanTransactionHelper;
+    private LoanAccountLockHelper loanAccountLockHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        loanCOBCatchUpHelper = new LoanCOBCatchUpHelper();
+        requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        requestSpec.header("Fineract-Platform-TenantId", "default");
+        responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+    }
+
+    @Test
+    public void testCatchUpInLockedInstance() {
+        try {
+            GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
+            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 2));
+            GlobalConfigurationHelper.updateValueForGlobalConfiguration(this.requestSpec, this.responseSpec, "10", "0");
+            loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
+            loanAccountLockHelper = new LoanAccountLockHelper(requestSpec, new ResponseSpecBuilder().expectStatusCode(202).build());
+
+            final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec);
+            Assertions.assertNotNull(clientID);
+
+            Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
+                    ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1"));
+            Assertions.assertNotNull(overdueFeeChargeId);
+
+            final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString());
+            Assertions.assertNotNull(loanProductID);
+            HashMap loanStatusHashMap;
+            final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020");
+
+            Assertions.assertNotNull(loanID);
+
+            loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
+            LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+            loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID);
+            LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+
+            String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID);
+            loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID,
+                    JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
+            LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 3, 2));
+            loanAccountLockHelper.placeSoftLockOnLoanAccount(loanID, "LOAN_INLINE_COB_PROCESSING", "Sample error");
+
+            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 10));
+
+            loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
+            loanCOBCatchUpHelper.executeLoanCOBCatchUp();
+
+            Utils.conditionalSleepWithMaxWait(30, 1000, () -> loanCOBCatchUpHelper.isLoanCOBCatchUpRunning());
+
+            GetLoansLoanIdResponse loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID);
+            Assertions.assertEquals(LocalDate.of(2020, 3, 9), loan.getLastClosedBusinessDate());
+
+            requestSpec = UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec);
+
+            final BatchRequest br1 = BatchHelper.repayLoanRequestWithGivenLoanId(4730L, loanID, "10", LocalDate.of(2020, 3, 10));
+
+            final List<BatchRequest> batchRequests = new ArrayList<>();
+
+            batchRequests.add(br1);
+
+            final String jsonifiedRequest = BatchHelper.toJsonString(batchRequests);
+
+            final List<BatchResponse> response = BatchHelper.postBatchRequestsWithoutEnclosingTransaction(this.requestSpec,
+                    this.responseSpec, jsonifiedRequest);
+            Assertions.assertEquals(HttpStatus.SC_OK, (long) response.get(0).getStatusCode(), "Verify Status Code 200 for Repayment");
+
+            loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanID);
+            Assertions.assertEquals(LocalDate.of(2020, 3, 9), loan.getLastClosedBusinessDate());
+        } finally {
+            requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+            requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+            requestSpec.header("Fineract-Platform-TenantId", "default");
+            responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+            GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
+            GlobalConfigurationHelper.updateValueForGlobalConfiguration(this.requestSpec, this.responseSpec, "10", "2");
+        }
+    }
+
+    private Integer createLoanProduct(final String chargeId) {
+        final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4")
+                .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1")
+                .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
+                .build(chargeId);
+        return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
+    }
+
+    private Integer applyForLoanApplication(final String clientID, final String loanProductID, final String savingsID, final String date) {
+
+        List<HashMap> collaterals = new ArrayList<>();
+        final Integer collateralId = CollateralManagementHelper.createCollateralProduct(this.requestSpec, this.responseSpec);
+        Assertions.assertNotNull(collateralId);
+        final Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(this.requestSpec, this.responseSpec, clientID,
+                collateralId);
+        Assertions.assertNotNull(clientCollateralId);
+        addCollaterals(collaterals, clientCollateralId, BigDecimal.valueOf(1));
+
+        final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("15,000.00").withLoanTermFrequency("4")
+                .withLoanTermFrequencyAsMonths().withNumberOfRepayments("4").withRepaymentEveryAfter("1")
+                .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments()
+                .withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod()
+                .withExpectedDisbursementDate(date).withSubmittedOnDate(date).withCollaterals(collaterals)
+                .build(clientID, loanProductID, savingsID);
+        return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+    }
+
+    private void addCollaterals(List<HashMap> collaterals, Integer collateralId, BigDecimal quantity) {
+        collaterals.add(collaterals(collateralId, quantity));
+    }
+
+    private HashMap<String, String> collaterals(Integer collateralId, BigDecimal quantity) {
+        HashMap<String, String> collateral = new HashMap<>(2);
+        collateral.put("clientCollateralId", collateralId.toString());
+        collateral.put("quantity", quantity.toString());
+        return collateral;
+    }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
index 29bcf04e9..618ce73b5 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
@@ -59,6 +59,7 @@ import java.util.Set;
 import java.util.TimeZone;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.http.conn.HttpHostConnectException;
 import org.slf4j.Logger;
@@ -144,6 +145,23 @@ public final class Utils {
         }
     }
 
+    /**
+     * Wait until the given condition is true or the maxRun is reached.
+     *
+     * @param maxRun
+     *            max number of times to run the condition
+     * @param waitInMs
+     *            wait time between evaluation in milliseconds
+     * @param waitCondition
+     *            condition to evaluate
+     */
+    public static void conditionalSleepWithMaxWait(int maxRun, int waitInMs, Supplier<Boolean> waitCondition) {
+        do {
+            sleep(waitInMs);
+            maxRun--;
+        } while (maxRun > 0 && waitCondition.get());
+    }
+
     private static void sleep(int seconds) {
         try {
             Thread.sleep(seconds * 1000);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanAccountLockHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanAccountLockHelper.java
index 983ae9ad7..5bfa9887f 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanAccountLockHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanAccountLockHelper.java
@@ -38,8 +38,13 @@ public class LoanAccountLockHelper extends IntegrationTest {
     }
 
     public String placeSoftLockOnLoanAccount(Integer loanId, String lockOwner) {
+        return placeSoftLockOnLoanAccount(loanId, lockOwner, null);
+    }
+
+    public String placeSoftLockOnLoanAccount(Integer loanId, String lockOwner, String error) {
         return Utils.performServerPost(requestSpec, responseSpec,
                 INTERNAL_PLACE_LOCK_ON_LOAN_ACCOUNT_URL + loanId + "/place-lock/" + lockOwner + "?" + Utils.TENANT_IDENTIFIER,
-                GSON.toJson(null));
+                error == null ? GSON.toJson(null) : error);
     }
+
 }
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanCOBCatchUpHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanCOBCatchUpHelper.java
index 5a253ec04..606fef58a 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanCOBCatchUpHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanCOBCatchUpHelper.java
@@ -18,6 +18,7 @@
  */
 package org.apache.fineract.integrationtests.common.loans;
 
+import java.util.Objects;
 import org.apache.fineract.client.models.GetOldestCOBProcessedLoanResponse;
 import org.apache.fineract.client.models.IsCatchUpRunningResponse;
 import org.apache.fineract.integrationtests.client.IntegrationTest;
@@ -27,6 +28,11 @@ public class LoanCOBCatchUpHelper extends IntegrationTest {
 
     public LoanCOBCatchUpHelper() {}
 
+    public boolean isLoanCOBCatchUpRunning() {
+        Response<IsCatchUpRunningResponse> response = executeGetLoanCatchUpStatus();
+        return Boolean.TRUE.equals(Objects.requireNonNull(response.body()).getIsCatchUpRunning());
+    }
+
     public Response<Void> executeLoanCOBCatchUp() {
         return okR(fineract().loanCobCatchUpApi.executeLoanCOBCatchUp());
     }
@@ -38,4 +44,5 @@ public class LoanCOBCatchUpHelper extends IntegrationTest {
     public Response<IsCatchUpRunningResponse> executeGetLoanCatchUpStatus() {
         return okR(fineract().loanCobCatchUpApi.isCatchUpRunning());
     }
+
 }