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 2d73c2522 FINERACT-1678: Lock under processing loan accounts
2d73c2522 is described below

commit 2d73c252268e22ea345be96537a01e089fbf068e
Author: Adam Saghy <[email protected]>
AuthorDate: Wed Sep 7 23:28:37 2022 +0200

    FINERACT-1678: Lock under processing loan accounts
---
 .../fineract/cob/COBBusinessStepServiceImpl.java   |   8 +
 .../cob/api/LoanAccountLockApiResource.java        |   2 +-
 .../{loan => common}/InitialisationTasklet.java    |  18 +--
 .../fineract/cob/domain/LoanAccountLock.java       |  21 ++-
 .../cob/domain/LoanAccountLockRepository.java      |  11 +-
 .../org/apache/fineract/cob/domain/LockOwner.java  |   2 +-
 .../BusinessStepException.java}                    |  12 +-
 .../LoanAccountWasAlreadyLocked.java}              |  10 +-
 .../LoanReadException.java}                        |  15 +-
 .../fineract/cob/listener/LoanItemListener.java    | 104 +++++++++++++
 .../fineract/cob/loan/ApplyLoanLockTasklet.java    |  85 +++++++++++
 ...ieveLoanIdService.java => LoanCOBConstant.java} |  12 +-
 .../cob/loan/LoanCOBManagerConfiguration.java      |  10 +-
 .../fineract/cob/loan/LoanCOBPartitioner.java      |  44 +++---
 .../cob/loan/LoanCOBWorkerConfiguration.java       |  97 +++++++-----
 .../fineract/cob/loan/LoanItemProcessor.java       |  24 +--
 .../apache/fineract/cob/loan/LoanItemReader.java   |  78 ++++++++++
 ...dLoanIdServiceImpl.java => LoanItemWriter.java} |  21 ++-
 .../RetrieveAllNonClosedLoanIdServiceImpl.java     |   2 +-
 .../fineract/cob/loan/RetrieveLoanIdService.java   |   2 +-
 .../serialization/ThrowableSerialization.java}     |  24 +--
 .../springbatch/ContextualMessage.java}            |   4 +-
 .../springbatch/InputChannelInterceptor.java}      |   9 +-
 .../springbatch/ManagerConfig.java}                |   8 +-
 .../springbatch/OutputChannelInterceptor.java}     |  17 ++-
 .../springbatch/PropertyService.java}              |   4 +-
 .../springbatch/PropertyServiceImpl.java}          |   8 +-
 .../springbatch/WorkerConfig.java}                 |   8 +-
 .../messagehandler/JmsBrokerConfiguration.java     |   2 +-
 .../messagehandler/JmsManagerConfig.java}          |  10 +-
 .../messagehandler/JmsWorkerConfig.java}           |  10 +-
 .../messagehandler/SpringEventManagerConfig.java}  |  10 +-
 .../messagehandler/SpringEventWorkerConfig.java}   |  10 +-
 .../conditions/JmsManagerCondition.java            |   2 +-
 .../conditions/JmsWorkerCondition.java             |   2 +-
 .../conditions/SpringEventManagerCondition.java    |   2 +-
 .../conditions/SpringEventWorkerCondition.java     |   2 +-
 .../loanaccount/domain/LoanRepository.java         |   2 +-
 .../db/changelog/tenant/changelog-tenant.xml       |   1 +
 .../parts/0048_rework_loan_account_locks.xml       |  48 ++++++
 .../cob/COBBusinessStepServiceStepDefinitions.java | 167 +++++++++++++++++++++
 .../InitialisationTaskletStepDefinitions.java      |  67 +++++++++
 .../listener/LoanItemListenerStepDefinitions.java  | 123 +++++++++++++++
 .../loan/ApplyLoanLockTaskletStepDefinitions.java  |  92 ++++++++++++
 .../loan/LoanCOBPartitionerStepDefinitions.java    | 113 ++++++++++++++
 .../cob/loan/LoanItemProcessorStepDefinitions.java |  82 ++++++++++
 .../cob/loan/LoanItemReaderStepDefinitions.java    |  99 ++++++++++++
 .../cob/loan/LoanItemWriterStepDefinitions.java    |  74 +++++++++
 .../src/test/resources/features/cob/cob.feature    |  72 +++++++++
 .../cob/common/cob.initialisation.step.feature     |  39 +++++
 .../cob/listener/cob.loan.item.listener.feature    |  50 ++++++
 .../features/cob/loan/cob.applylock.step.feature   |  39 +++++
 .../features/cob/loan/cob.loan.processor.feature   |  39 +++++
 .../features/cob/loan/cob.loan.reader.feature      |  52 +++++++
 .../features/cob/loan/cob.loan.writer.feature      |  39 +++++
 .../features/cob/loan/cob.partitioner.feature      |  40 +++++
 56 files changed, 1763 insertions(+), 185 deletions(-)

diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java
index bce1fb4b0..363efda84 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/COBBusinessStepServiceImpl.java
@@ -23,8 +23,10 @@ import java.util.List;
 import java.util.Optional;
 import java.util.TreeMap;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.cob.domain.BatchBusinessStep;
 import org.apache.fineract.cob.domain.BatchBusinessStepRepository;
+import org.apache.fineract.cob.exceptions.BusinessStepException;
 import 
org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
 import org.apache.fineract.infrastructure.core.domain.ActionContext;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
@@ -34,6 +36,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.stereotype.Service;
 
 @Service
+@Slf4j
 @RequiredArgsConstructor
 public class COBBusinessStepServiceImpl implements COBBusinessStepService {
 
@@ -43,10 +46,15 @@ public class COBBusinessStepServiceImpl implements 
COBBusinessStepService {
 
     @Override
     public <T extends COBBusinessStep<S>, S extends AbstractPersistableCustom> 
S run(TreeMap<Long, String> executionMap, S item) {
+        if (executionMap == null || executionMap.isEmpty()) {
+            throw new BusinessStepException("Execution map is empty! COB 
Business step execution skipped!");
+        }
         for (String businessStep : executionMap.values()) {
             try {
                 COBBusinessStep<S> businessStepBean = (COBBusinessStep<S>) 
applicationContext.getBean(businessStep);
                 item = businessStepBean.execute(item);
+            } catch (Exception e) {
+                throw new BusinessStepException("Error happened during 
business step execution", e);
             } finally {
                 // Fallback to COB action context after each business step
                 ThreadLocalContextUtil.setActionContext(ActionContext.COB);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanAccountLockApiResource.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanAccountLockApiResource.java
index dcae39083..69f0a352d 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanAccountLockApiResource.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/api/LoanAccountLockApiResource.java
@@ -65,7 +65,7 @@ public class LoanAccountLockApiResource {
     @Operation(summary = "List locked loan accounts", description = "Returns 
the locked loan IDs")
     @ApiResponses({
             @ApiResponse(responseCode = "200", description = "OK", content = 
@Content(array = @ArraySchema(schema = @Schema(implementation = 
LoanAccountLockApiResourceSwagger.GetLoanAccountLockResponse.class)))) })
-    public String retrieveAllConfiguredBusinessStep(@Context final UriInfo 
uriInfo, @QueryParam("page") String page,
+    public String retrieveLockedAccounts(@Context final UriInfo uriInfo, 
@QueryParam("page") String page,
             @QueryParam("limit") String limit) {
 
         List<LoanAccountLock> lockedLoanAccounts = 
loanAccountLockService.getLockedLoanAccountByPage(Integer.parseInt(page),
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InitialisationTasklet.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/common/InitialisationTasklet.java
similarity index 78%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/loan/InitialisationTasklet.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/cob/common/InitialisationTasklet.java
index f352c17b8..20be30778 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/InitialisationTasklet.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/common/InitialisationTasklet.java
@@ -16,17 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.loan;
+package org.apache.fineract.cob.common;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.useradministration.domain.AppUser;
 import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
 import org.jetbrains.annotations.NotNull;
-import org.springframework.batch.core.ExitStatus;
 import org.springframework.batch.core.StepContribution;
-import org.springframework.batch.core.StepExecution;
-import org.springframework.batch.core.StepExecutionListener;
 import org.springframework.batch.core.scope.context.ChunkContext;
 import org.springframework.batch.core.step.tasklet.Tasklet;
 import org.springframework.batch.repeat.RepeatStatus;
@@ -36,10 +33,9 @@ import 
org.springframework.security.core.context.SecurityContextHolder;
 
 @Slf4j
 @RequiredArgsConstructor
-public class InitialisationTasklet implements Tasklet, StepExecutionListener {
+public class InitialisationTasklet implements Tasklet {
 
     private final AppUserRepositoryWrapper userRepository;
-    private StepExecution stepExecution;
 
     @Override
     public RepeatStatus execute(@NotNull StepContribution contribution, 
@NotNull ChunkContext chunkContext) throws Exception {
@@ -49,14 +45,4 @@ public class InitialisationTasklet implements Tasklet, 
StepExecutionListener {
         SecurityContextHolder.getContext().setAuthentication(auth);
         return RepeatStatus.FINISHED;
     }
-
-    @Override
-    public void beforeStep(@NotNull StepExecution stepExecution) {
-        this.stepExecution = stepExecution;
-    }
-
-    @Override
-    public ExitStatus afterStep(@NotNull StepExecution stepExecution) {
-        return ExitStatus.COMPLETED;
-    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java
index 917dbf480..e490bde89 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LoanAccountLock.java
@@ -18,7 +18,7 @@
  */
 package org.apache.fineract.cob.domain;
 
-import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.EnumType;
@@ -28,6 +28,7 @@ import javax.persistence.Table;
 import javax.persistence.Version;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
 
 @Entity
 @Table(name = "m_loan_account_locks")
@@ -48,11 +49,27 @@ public class LoanAccountLock {
     private LockOwner lockOwner;
 
     @Column(name = "lock_placed_on", nullable = false)
-    private LocalDateTime lockPlacedOn;
+    private OffsetDateTime lockPlacedOn;
 
     @Column(name = "error")
     private String error;
 
     @Column(name = "stacktrace")
     private String stacktrace;
+
+    public LoanAccountLock(Long loanId, LockOwner lockOwner) {
+        this.loanId = loanId;
+        this.lockOwner = lockOwner;
+        this.lockPlacedOn = DateUtils.getOffsetDateTimeOfTenant();
+    }
+
+    public void setError(String errorMessage, String stacktrace) {
+        this.error = errorMessage;
+        this.stacktrace = stacktrace;
+    }
+
+    public void setNewLockOwner(LockOwner newLockOwner) {
+        this.lockOwner = newLockOwner;
+        this.lockPlacedOn = DateUtils.getOffsetDateTimeOfTenant();
+    }
 }
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 5398e8773..e25da035a 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
@@ -18,7 +18,16 @@
  */
 package org.apache.fineract.cob.domain;
 
+import java.util.List;
+import java.util.Optional;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
-public interface LoanAccountLockRepository extends 
JpaRepository<LoanAccountLock, Long>, JpaSpecificationExecutor<LoanAccountLock> 
{}
+public interface LoanAccountLockRepository extends 
JpaRepository<LoanAccountLock, Long>, JpaSpecificationExecutor<LoanAccountLock> 
{
+
+    Optional<LoanAccountLock> findByLoanIdAndLockOwner(Long loanId, LockOwner 
lockOwner);
+
+    void deleteByLoanIdInAndLockOwner(List<Long> loanIds, LockOwner lockOwner);
+
+    List<LoanAccountLock> findAllByLoanIdIn(List<Long> loanIds);
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LockOwner.java 
b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LockOwner.java
index 5b8943348..5a6580d6c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LockOwner.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/domain/LockOwner.java
@@ -19,5 +19,5 @@
 package org.apache.fineract.cob.domain;
 
 public enum LockOwner {
-    LOAN_COB, LOAN_INLINE_COB;
+    LOAN_COB_PARTITIONING, LOAN_COB_CHUNK_PROCESSING, 
LOAN_INLINE_COB_PROCESSING;
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
similarity index 73%
copy from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
index 58c6d534c..a11a5c57a 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
@@ -16,11 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.cob.exceptions;
 
-public interface COBPropertyService {
+public class BusinessStepException extends RuntimeException {
 
-    Integer getPartitionSize(String jobName);
+    public BusinessStepException(String message) {
+        super(message);
+    }
 
-    Integer getChunkSize(String jobName);
+    public BusinessStepException(String message, Throwable t) {
+        super(message, t);
+    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanAccountWasAlreadyLocked.java
similarity index 76%
copy from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanAccountWasAlreadyLocked.java
index 58c6d534c..c33f4cd0f 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanAccountWasAlreadyLocked.java
@@ -16,11 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.cob.exceptions;
 
-public interface COBPropertyService {
+public class LoanAccountWasAlreadyLocked extends Exception {
 
-    Integer getPartitionSize(String jobName);
-
-    Integer getChunkSize(String jobName);
+    public LoanAccountWasAlreadyLocked(Long loanId) {
+        super(String.format("Loan is in already locked state! loanId: %d", 
loanId));
+    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java
similarity index 70%
copy from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java
index 58c6d534c..482f4f55b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/LoanReadException.java
@@ -16,11 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.cob.exceptions;
 
-public interface COBPropertyService {
+public class LoanReadException extends Exception {
 
-    Integer getPartitionSize(String jobName);
+    private final Long id;
 
-    Integer getChunkSize(String jobName);
+    public LoanReadException(Long id, Throwable t) {
+        super(String.format("Loan is in already locked state! loanId: %d", 
id), t);
+        this.id = id;
+    }
+
+    public Long getId() {
+        return id;
+    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/listener/LoanItemListener.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/LoanItemListener.java
new file mode 100644
index 000000000..12a55ae4b
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/listener/LoanItemListener.java
@@ -0,0 +1,104 @@
+/**
+ * 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.cob.listener;
+
+import static 
org.springframework.transaction.TransactionDefinition.PROPAGATION_REQUIRES_NEW;
+
+import java.util.List;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.domain.LoanAccountLock;
+import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.exceptions.LoanReadException;
+import 
org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import 
org.apache.fineract.infrastructure.core.serialization.ThrowableSerialization;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.batch.core.annotation.OnProcessError;
+import org.springframework.batch.core.annotation.OnReadError;
+import org.springframework.batch.core.annotation.OnSkipInProcess;
+import org.springframework.batch.core.annotation.OnSkipInRead;
+import org.springframework.batch.core.annotation.OnSkipInWrite;
+import org.springframework.batch.core.annotation.OnWriteError;
+import org.springframework.transaction.TransactionStatus;
+import 
org.springframework.transaction.support.TransactionCallbackWithoutResult;
+import org.springframework.transaction.support.TransactionTemplate;
+
+@Slf4j
+@RequiredArgsConstructor
+public class LoanItemListener {
+
+    private final LoanAccountLockRepository accountLockRepository;
+
+    private final TransactionTemplate transactionTemplate;
+
+    private void updateAccountLockWithError(List<Long> loanIds, String msg, 
Throwable e) {
+        transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
+        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
+
+            @Override
+            protected void doInTransactionWithoutResult(@NotNull 
TransactionStatus status) {
+                for (Long loanId : loanIds) {
+                    Optional<LoanAccountLock> loanAccountLock = 
accountLockRepository.findByLoanIdAndLockOwner(loanId,
+                            LockOwner.LOAN_COB_CHUNK_PROCESSING);
+                    loanAccountLock.ifPresent(
+                            accountLock -> 
accountLock.setError(String.format(msg, loanId), 
ThrowableSerialization.serialize(e)));
+                }
+            }
+        });
+    }
+
+    @OnReadError
+    public void onReadError(Exception e) {
+        LoanReadException ee = (LoanReadException) e;
+        log.warn("Error was triggered during reading of Loan (id={}) due to: 
{}", ee.getId(), ThrowableSerialization.serialize(e));
+        updateAccountLockWithError(List.of(ee.getId()), "Loan (id: %d) reading 
is failed", e);
+    }
+
+    @OnProcessError
+    public void onProcessError(@NotNull Loan item, Exception e) {
+        log.warn("Error was triggered during processing of Loan (id={}) due 
to: {}", item.getId(), ThrowableSerialization.serialize(e));
+        updateAccountLockWithError(List.of(item.getId()), "Loan (id: %d) 
processing is failed", e);
+    }
+
+    @OnWriteError
+    public void onWriteError(Exception e, @NotNull List<? extends Loan> items) 
{
+        List<Long> loanIds = 
items.stream().map(AbstractPersistableCustom::getId).toList();
+        log.warn("Error was triggered during writing of Loans (ids={}) due to: 
{}", loanIds, ThrowableSerialization.serialize(e));
+
+        updateAccountLockWithError(loanIds, "Loan (id: %d) writing is failed", 
e);
+    }
+
+    @OnSkipInRead
+    public void onSkipInRead(@NotNull Exception e) {
+        log.warn("Skipping was triggered during read!");
+    }
+
+    @OnSkipInProcess
+    public void onSkipInProcess(@NotNull Loan item, @NotNull Exception e) {
+        log.warn("Skipping was triggered during processing of Loan (id={})", 
item.getId());
+    }
+
+    @OnSkipInWrite
+    public void onSkipInWrite(@NotNull Loan item, @NotNull Exception e) {
+        log.warn("Skipping was triggered during writing of Loan (id={})", 
item.getId());
+    }
+}
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
new file mode 100644
index 000000000..9699d3caa
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/ApplyLoanLockTasklet.java
@@ -0,0 +1,85 @@
+/**
+ * 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.cob.loan;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.domain.LoanAccountLock;
+import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.batch.core.StepContribution;
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.core.step.tasklet.Tasklet;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.repeat.RepeatStatus;
+
+@Slf4j
+@RequiredArgsConstructor
+public class ApplyLoanLockTasklet implements Tasklet {
+
+    private final LoanAccountLockRepository accountLockRepository;
+
+    @Override
+    public RepeatStatus execute(@NotNull StepContribution contribution, 
@NotNull ChunkContext chunkContext) throws Exception {
+        ExecutionContext executionContext = 
contribution.getStepExecution().getExecutionContext();
+        List<Long> loanIds = (List<Long>) 
executionContext.get(LoanCOBConstant.LOAN_IDS);
+        List<Long> remainingLoanIds = new ArrayList<>(loanIds);
+
+        List<LoanAccountLock> accountLocks = 
accountLockRepository.findAllByLoanIdIn(remainingLoanIds);
+
+        List<Long> alreadyHardLockedAccountIds = accountLocks.stream()
+                .filter(e -> 
LockOwner.LOAN_COB_CHUNK_PROCESSING.equals(e.getLockOwner())).map(LoanAccountLock::getLoanId).toList();
+
+        List<Long> alreadyUnderProcessingAccountIds = accountLocks.stream()
+                .filter(e -> 
LockOwner.LOAN_INLINE_COB_PROCESSING.equals(e.getLockOwner())).map(LoanAccountLock::getLoanId).toList();
+
+        Map<Long, LoanAccountLock> alreadySoftLockedAccountsMap = 
accountLocks.stream()
+                .filter(e -> 
LockOwner.LOAN_COB_PARTITIONING.equals(e.getLockOwner()))
+                .collect(Collectors.toMap(LoanAccountLock::getLoanId, 
Function.identity()));
+
+        remainingLoanIds.removeAll(alreadyHardLockedAccountIds);
+        remainingLoanIds.removeAll(alreadyUnderProcessingAccountIds);
+
+        for (Long loanId : remainingLoanIds) {
+            LoanAccountLock loanAccountLock = addLock(loanId, 
alreadySoftLockedAccountsMap);
+            accountLockRepository.save(loanAccountLock);
+        }
+
+        
executionContext.put(LoanCOBWorkerConfiguration.ALREADY_LOCKED_LOAN_IDS, new 
ArrayList<>(alreadyUnderProcessingAccountIds));
+        return RepeatStatus.FINISHED;
+    }
+
+    private LoanAccountLock addLock(Long loanId, Map<Long, LoanAccountLock> 
alreadySoftLockedAccountsMap) {
+        LoanAccountLock loanAccountLock;
+        if (alreadySoftLockedAccountsMap.containsKey(loanId)) {
+            // Upgrade lock
+            loanAccountLock = alreadySoftLockedAccountsMap.get(loanId);
+            
loanAccountLock.setNewLockOwner(LockOwner.LOAN_COB_CHUNK_PROCESSING);
+        } else {
+            loanAccountLock = new LoanAccountLock(loanId, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+        }
+        return loanAccountLock;
+    }
+}
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/LoanCOBConstant.java
similarity index 67%
copy from 
fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveLoanIdService.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBConstant.java
index 5147b01e9..e9527d6c0 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/LoanCOBConstant.java
@@ -18,9 +18,15 @@
  */
 package org.apache.fineract.cob.loan;
 
-import java.util.List;
+public final class LoanCOBConstant {
 
-public interface RetrieveLoanIdService {
+    public static final String JOB_NAME = "LOAN_COB";
+    public static final String LOAN_COB_JOB_NAME = "LOAN_CLOSE_OF_BUSINESS";
+    public static final String LOAN_IDS = "loanIds";
+    public static final String BUSINESS_STEP_MAP = "businessStepMap";
+    public static final String LOAN_COB_WORKER_STEP = "loanCOBWorkerStep";
 
-    List<Integer> retrieveLoanIds();
+    private LoanCOBConstant() {
+
+    }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java
index 3e50260e1..c4911703c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBManagerConfiguration.java
@@ -19,9 +19,9 @@
 package org.apache.fineract.cob.loan;
 
 import org.apache.fineract.cob.COBBusinessStepService;
-import org.apache.fineract.cob.COBPropertyService;
 import org.apache.fineract.cob.listener.COBExecutionListenerRunner;
 import org.apache.fineract.infrastructure.jobs.service.JobName;
+import org.apache.fineract.infrastructure.springbatch.PropertyService;
 import org.springframework.batch.core.Job;
 import org.springframework.batch.core.Step;
 import 
org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
@@ -47,7 +47,7 @@ public class LoanCOBManagerConfiguration {
     @Autowired
     private RemotePartitioningManagerStepBuilderFactory stepBuilderFactory;
     @Autowired
-    private COBPropertyService cobPropertyService;
+    private PropertyService propertyService;
     @Autowired
     private DirectChannel outboundRequests;
     @Autowired
@@ -63,13 +63,13 @@ public class LoanCOBManagerConfiguration {
 
     @Bean
     public LoanCOBPartitioner partitioner() {
-        return new LoanCOBPartitioner(cobPropertyService, 
cobBusinessStepService, jobOperator, jobExplorer, retrieveLoanIdService);
+        return new LoanCOBPartitioner(propertyService, cobBusinessStepService, 
jobOperator, jobExplorer, retrieveLoanIdService);
     }
 
     @Bean
     public Step loanCOBStep() {
-        return 
stepBuilderFactory.get(JobName.LOAN_COB.name()).partitioner("Loan COB worker", 
partitioner()).outputChannel(outboundRequests)
-                .build();
+        return 
stepBuilderFactory.get(JobName.LOAN_COB.name()).partitioner(LoanCOBConstant.LOAN_COB_WORKER_STEP,
 partitioner())
+                .outputChannel(outboundRequests).build();
     }
 
     @Bean(name = "loanCOBJob")
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java
index 39bb395d5..aa675a91e 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBPartitioner.java
@@ -27,9 +27,9 @@ import java.util.TreeMap;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.cob.COBBusinessStepService;
-import org.apache.fineract.cob.COBPropertyService;
 import org.apache.fineract.infrastructure.jobs.service.JobName;
-import org.jetbrains.annotations.Nullable;
+import org.apache.fineract.infrastructure.springbatch.PropertyService;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.batch.core.JobExecution;
 import org.springframework.batch.core.explore.JobExplorer;
 import org.springframework.batch.core.launch.JobExecutionNotRunningException;
@@ -42,44 +42,48 @@ import org.springframework.batch.item.ExecutionContext;
 @RequiredArgsConstructor
 public class LoanCOBPartitioner implements Partitioner {
 
-    private final COBPropertyService cobPropertyService;
+    public static final String PARTITION_PREFIX = "partition_";
+
+    private final PropertyService propertyService;
     private final COBBusinessStepService cobBusinessStepService;
     private final JobOperator jobOperator;
     private final JobExplorer jobExplorer;
     private final RetrieveLoanIdService retrieveLoanIdService;
 
-    private static final String PARTITION_PREFIX = "partition_";
-    private static final String JOB_NAME = "LOAN_COB";
-    private static final String LOAN_COB_JOB_NAME = "LOAN_CLOSE_OF_BUSINESS";
-
+    @NotNull
     @Override
     public Map<String, ExecutionContext> partition(int gridSize) {
-        int partitionSize = cobPropertyService.getPartitionSize(JOB_NAME);
+        int partitionSize = 
propertyService.getPartitionSize(LoanCOBConstant.JOB_NAME);
         TreeMap<Long, String> cobBusinessStepMap = 
cobBusinessStepService.getCOBBusinessStepMap(LoanCOBBusinessStep.class,
-                LOAN_COB_JOB_NAME);
+                LoanCOBConstant.LOAN_COB_JOB_NAME);
         if (cobBusinessStepMap.isEmpty()) {
-            return stopJobExecution();
+            stopJobExecution();
+            return Map.of();
         }
         return getPartitions(partitionSize, cobBusinessStepMap);
     }
 
     private Map<String, ExecutionContext> getPartitions(int partitionSize, 
TreeMap<Long, String> cobBusinessStepMap) {
         Map<String, ExecutionContext> partitions = new HashMap<>();
-        List<Integer> allNonClosedLoanIds = 
retrieveLoanIdService.retrieveLoanIds();
+        List<Long> allNonClosedLoanIds = 
retrieveLoanIdService.retrieveLoanIds();
         if (allNonClosedLoanIds.isEmpty()) {
-            return stopJobExecution();
+            stopJobExecution();
+            return Map.of();
         }
-        int partitionIndex = 0;
+        int partitionIndex = 1;
+        int remainingSpace = 0;
         createNewPartition(partitions, partitionIndex, cobBusinessStepMap);
-        for (Integer allNonClosedLoanId : allNonClosedLoanIds) {
-            if (partitions.get(PARTITION_PREFIX + partitionIndex).size() == 
partitionSize) {
+        for (Long allNonClosedLoanId : allNonClosedLoanIds) {
+            if (remainingSpace == partitionSize) {
                 partitionIndex++;
                 createNewPartition(partitions, partitionIndex, 
cobBusinessStepMap);
+                remainingSpace = 0;
             }
             String key = PARTITION_PREFIX + partitionIndex;
             ExecutionContext executionContext = partitions.get(key);
-            List<Integer> data = (List<Integer>) 
executionContext.get("loanIds");
+            List<Long> data = (List<Long>) 
executionContext.get(LoanCOBConstant.LOAN_IDS);
             data.add(allNonClosedLoanId);
+            remainingSpace++;
         }
         return partitions;
     }
@@ -87,13 +91,12 @@ public class LoanCOBPartitioner implements Partitioner {
     private void createNewPartition(Map<String, ExecutionContext> partitions, 
int partitionIndex,
             TreeMap<Long, String> cobBusinessStepMap) {
         ExecutionContext executionContext = new ExecutionContext();
-        executionContext.put("loanIds", new ArrayList<Integer>());
-        executionContext.put("BusinessStepMap", cobBusinessStepMap);
+        executionContext.put(LoanCOBConstant.LOAN_IDS, new ArrayList<Long>());
+        executionContext.put(LoanCOBConstant.BUSINESS_STEP_MAP, 
cobBusinessStepMap);
         partitions.put(PARTITION_PREFIX + partitionIndex, executionContext);
     }
 
-    @Nullable
-    private Map<String, ExecutionContext> stopJobExecution() {
+    private void stopJobExecution() {
         Set<JobExecution> runningJobExecutions = 
jobExplorer.findRunningJobExecutions(JobName.LOAN_COB.name());
         for (JobExecution jobExecution : runningJobExecutions) {
             try {
@@ -103,6 +106,5 @@ public class LoanCOBPartitioner implements Partitioner {
                 throw new RuntimeException(e);
             }
         }
-        return null;
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java
index e816f7cfe..b666dc33b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanCOBWorkerConfiguration.java
@@ -18,42 +18,37 @@
  */
 package org.apache.fineract.cob.loan;
 
-import java.util.ArrayList;
-import java.util.List;
 import org.apache.fineract.cob.COBBusinessStepService;
-import org.apache.fineract.cob.COBPropertyService;
+import org.apache.fineract.cob.common.InitialisationTasklet;
+import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.listener.LoanItemListener;
 import org.apache.fineract.infrastructure.jobs.service.JobName;
+import org.apache.fineract.infrastructure.springbatch.PropertyService;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
 import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
-import org.springframework.batch.core.Job;
 import org.springframework.batch.core.Step;
-import 
org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
 import org.springframework.batch.core.configuration.annotation.StepScope;
 import org.springframework.batch.core.job.builder.FlowBuilder;
 import org.springframework.batch.core.job.flow.Flow;
-import org.springframework.batch.core.launch.support.RunIdIncrementer;
+import 
org.springframework.batch.core.listener.ExecutionContextPromotionListener;
 import 
org.springframework.batch.integration.partition.RemotePartitioningWorkerStepBuilderFactory;
-import org.springframework.batch.item.ItemProcessor;
-import org.springframework.batch.item.ItemReader;
-import org.springframework.batch.item.data.RepositoryItemWriter;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.integration.channel.QueueChannel;
+import org.springframework.transaction.support.TransactionTemplate;
 
 @Configuration
 @ConditionalOnProperty(value = "fineract.mode.batch-worker-enabled", 
havingValue = "true")
 public class LoanCOBWorkerConfiguration {
 
-    @Autowired
-    private JobBuilderFactory jobBuilderFactory;
+    public static final String ALREADY_LOCKED_LOAN_IDS = 
"alreadyLockedLoanIds";
     @Autowired
     private RemotePartitioningWorkerStepBuilderFactory stepBuilderFactory;
     @Autowired
-    private COBPropertyService cobPropertyService;
+    private PropertyService propertyService;
     @Autowired
     private LoanRepository loanRepository;
     @Autowired
@@ -61,33 +56,40 @@ public class LoanCOBWorkerConfiguration {
     @Autowired
     private COBBusinessStepService cobBusinessStepService;
     @Autowired
+    private LoanAccountLockRepository accountLockRepository;
+    @Autowired
     private AppUserRepositoryWrapper userRepository;
+    @Autowired
+    private TransactionTemplate transactionTemplate;
 
-    @Bean
-    public Step loanBusinessStep() {
-        return stepBuilderFactory.get("Loan COB 
worker").inputChannel(inboundRequests)
-                .<Loan, 
Loan>chunk(cobPropertyService.getChunkSize(JobName.LOAN_COB.name())).reader(itemReader(null))
-                .processor(itemProcessor()).writer(itemWriter()).build();
-    }
-
-    @Bean(name = "Loan COB worker")
+    @Bean(name = LoanCOBConstant.LOAN_COB_WORKER_STEP)
     public Step loanCOBWorkerStep() {
         return stepBuilderFactory.get("Loan COB worker - 
Step").inputChannel(inboundRequests).flow(flow()).build();
     }
 
     @Bean
-    public Job loanCOBWorkerJob() {
-        return jobBuilderFactory.get("Loan COB 
worker").start(loanCOBWorkerStep()).incrementer(new RunIdIncrementer()).build();
+    public Flow flow() {
+        return new 
FlowBuilder<Flow>("cobFlow").start(initialisationStep()).next(applyLockStep()).next(loanBusinessStep()).build();
     }
 
     @Bean
-    public Flow flow() {
-        return new 
FlowBuilder<Flow>("cobFlow").start(initialisationStep()).next(loanBusinessStep()).build();
+    public Step initialisationStep() {
+        return stepBuilderFactory.get("Initialisation - 
Step").inputChannel(inboundRequests).tasklet(initialiseContext()).build();
     }
 
     @Bean
-    public Step initialisationStep() {
-        return stepBuilderFactory.get("Initalisation - 
Step").inputChannel(inboundRequests).tasklet(initialiseContext()).build();
+    public Step loanBusinessStep() {
+        return stepBuilderFactory.get("Loan COB worker - 
Step").inputChannel(inboundRequests)
+                .<Loan, 
Loan>chunk(propertyService.getChunkSize(JobName.LOAN_COB.name())).reader(cobWorkerItemReader())
+                
.processor(cobWorkerItemProcessor()).writer(cobWorkerItemWriter()).faultTolerant().skip(Exception.class)
+                
.skipLimit(propertyService.getChunkSize(JobName.LOAN_COB.name()) + 
1).listener(loanItemListener())
+                .listener(promotionListener()).build();
+    }
+
+    @Bean
+    public Step applyLockStep() {
+        return stepBuilderFactory.get("Apply lock - 
Step").inputChannel(inboundRequests).tasklet(applyLock()).listener(promotionListener())
+                .build();
     }
 
     @Bean
@@ -96,28 +98,41 @@ public class LoanCOBWorkerConfiguration {
         return new InitialisationTasklet(userRepository);
     }
 
+    @Bean
+    public LoanItemListener loanItemListener() {
+        return new LoanItemListener(accountLockRepository, 
transactionTemplate);
+    }
+
     @Bean
     @StepScope
-    public ItemReader<Loan> 
itemReader(@Value("#{stepExecutionContext['loanIds']}") List<Integer> data) {
-        List<Integer> remainingData = new ArrayList<>(data);
-        return () -> {
-            if (remainingData.size() > 0) {
-                return 
loanRepository.findById(remainingData.remove(0).longValue()).orElse(null);
-            }
-            return null;
-        };
+    public ApplyLoanLockTasklet applyLock() {
+        return new ApplyLoanLockTasklet(accountLockRepository);
     }
 
     @Bean
-    public ItemProcessor<Loan, Loan> itemProcessor() {
+    @StepScope
+    public LoanItemReader cobWorkerItemReader() {
+        return new LoanItemReader(loanRepository);
+    }
+
+    @Bean
+    @StepScope
+    public LoanItemProcessor cobWorkerItemProcessor() {
         return new LoanItemProcessor(cobBusinessStepService);
     }
 
     @Bean
-    public RepositoryItemWriter<Loan> itemWriter() {
-        RepositoryItemWriter<Loan> writer = new RepositoryItemWriter<>();
-        writer.setRepository(loanRepository);
-        writer.setMethodName("save");
-        return writer;
+    @StepScope
+    public LoanItemWriter cobWorkerItemWriter() {
+        LoanItemWriter repositoryItemWriter = new 
LoanItemWriter(accountLockRepository);
+        repositoryItemWriter.setRepository(loanRepository);
+        return repositoryItemWriter;
+    }
+
+    @Bean
+    public ExecutionContextPromotionListener promotionListener() {
+        ExecutionContextPromotionListener listener = new 
ExecutionContextPromotionListener();
+        listener.setKeys(new String[] { ALREADY_LOCKED_LOAN_IDS });
+        return listener;
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java
 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java
index 4cb23a12b..bd88f5b15 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemProcessor.java
@@ -20,36 +20,40 @@ package org.apache.fineract.cob.loan;
 
 import java.util.TreeMap;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.cob.COBBusinessStepService;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.batch.core.ExitStatus;
 import org.springframework.batch.core.StepExecution;
-import org.springframework.batch.core.StepExecutionListener;
+import org.springframework.batch.core.annotation.AfterStep;
+import org.springframework.batch.core.annotation.BeforeStep;
 import org.springframework.batch.item.ExecutionContext;
 import org.springframework.batch.item.ItemProcessor;
-import org.springframework.stereotype.Component;
 
-@Component
 @RequiredArgsConstructor
-public class LoanItemProcessor implements ItemProcessor<Loan, Loan>, 
StepExecutionListener {
+@Slf4j
+public class LoanItemProcessor implements ItemProcessor<Loan, Loan> {
 
     private final COBBusinessStepService cobBusinessStepService;
     private StepExecution stepExecution;
 
-    @Override
-    public void beforeStep(StepExecution stepExecution) {
+    @BeforeStep
+    public void beforeStep(@NotNull StepExecution stepExecution) {
         this.stepExecution = stepExecution;
     }
 
     @Override
-    public Loan process(Loan item) throws Exception {
+    public Loan process(@NotNull Loan item) throws Exception {
         ExecutionContext executionContext = 
stepExecution.getExecutionContext();
-        TreeMap<Long, String> businessStepMap = (TreeMap<Long, String>) 
executionContext.get("BusinessStepMap");
+        TreeMap<Long, String> businessStepMap = (TreeMap<Long, String>) 
executionContext.get(LoanCOBConstant.BUSINESS_STEP_MAP);
+
         return cobBusinessStepService.run(businessStepMap, item);
     }
 
-    @Override
-    public ExitStatus afterStep(StepExecution stepExecution) {
+    @AfterStep
+    public ExitStatus afterStep(@NotNull StepExecution stepExecution) {
         return ExitStatus.COMPLETED;
     }
+
 }
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
new file mode 100644
index 000000000..15726e713
--- /dev/null
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemReader.java
@@ -0,0 +1,78 @@
+/**
+ * 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.cob.loan;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.exceptions.LoanAccountWasAlreadyLocked;
+import org.apache.fineract.cob.exceptions.LoanReadException;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import 
org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.annotation.AfterStep;
+import org.springframework.batch.core.annotation.BeforeStep;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemReader;
+
+@Slf4j
+@RequiredArgsConstructor
+public class LoanItemReader implements ItemReader<Loan> {
+
+    private final LoanRepository loanRepository;
+    private List<Long> alreadyLockedAccounts;
+    private List<Long> remainingData;
+    private Long loanId;
+
+    @BeforeStep
+    public void beforeStep(@NotNull StepExecution stepExecution) {
+        ExecutionContext executionContext = 
stepExecution.getExecutionContext();
+        ExecutionContext jobExecutionContext = 
stepExecution.getJobExecution().getExecutionContext();
+        List<Long> loanIds = (List<Long>) 
executionContext.get(LoanCOBConstant.LOAN_IDS);
+        alreadyLockedAccounts = (List<Long>) 
jobExecutionContext.get(LoanCOBWorkerConfiguration.ALREADY_LOCKED_LOAN_IDS);
+        remainingData = new ArrayList<>(loanIds);
+    }
+
+    @Override
+    public Loan read() throws Exception {
+        try {
+            if (remainingData.size() > 0) {
+                loanId = remainingData.remove(0);
+                if (alreadyLockedAccounts != null && 
alreadyLockedAccounts.remove(loanId)) {
+                    throw new LoanAccountWasAlreadyLocked(loanId);
+                }
+
+                return loanRepository.findById(loanId).orElseThrow(() -> new 
LoanNotFoundException(loanId));
+            }
+        } catch (Exception e) {
+            throw new LoanReadException(loanId, e);
+        }
+        return null;
+
+    }
+
+    @AfterStep
+    public ExitStatus afterStep(@NotNull StepExecution stepExecution) {
+        return ExitStatus.COMPLETED;
+    }
+}
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/LoanItemWriter.java
similarity index 53%
copy from 
fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/cob/loan/LoanItemWriter.java
index 1f84d1f84..bb5d2e6e9 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/LoanItemWriter.java
@@ -20,15 +20,26 @@ package org.apache.fineract.cob.loan;
 
 import java.util.List;
 import lombok.RequiredArgsConstructor;
-import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.domain.LockOwner;
+import 
org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.batch.item.data.RepositoryItemWriter;
 
+@Slf4j
 @RequiredArgsConstructor
-public class RetrieveAllNonClosedLoanIdServiceImpl implements 
RetrieveLoanIdService {
+public class LoanItemWriter extends RepositoryItemWriter<Loan> {
 
-    private final LoanRepository loanRepository;
+    private final LoanAccountLockRepository accountLockRepository;
 
     @Override
-    public List<Integer> retrieveLoanIds() {
-        return loanRepository.findAllNonClosedLoanIds();
+    public void write(@NotNull List<? extends Loan> items) throws Exception {
+        super.write(items);
+        List<Long> loanIds = 
items.stream().map(AbstractPersistableCustom::getId).toList();
+
+        accountLockRepository.deleteByLoanIdInAndLockOwner(loanIds, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
     }
+
 }
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 1f84d1f84..f4efc788e 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
@@ -28,7 +28,7 @@ public class RetrieveAllNonClosedLoanIdServiceImpl implements 
RetrieveLoanIdServ
     private final LoanRepository loanRepository;
 
     @Override
-    public List<Integer> retrieveLoanIds() {
+    public List<Long> retrieveLoanIds() {
         return loanRepository.findAllNonClosedLoanIds();
     }
 }
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 5147b01e9..266489ecb 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
@@ -22,5 +22,5 @@ import java.util.List;
 
 public interface RetrieveLoanIdService {
 
-    List<Integer> retrieveLoanIds();
+    List<Long> retrieveLoanIds();
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java
similarity index 61%
copy from 
fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
copy to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java
index 1f84d1f84..94b0b0209 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/RetrieveAllNonClosedLoanIdServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/serialization/ThrowableSerialization.java
@@ -16,19 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.loan;
+package org.apache.fineract.infrastructure.core.serialization;
 
-import java.util.List;
-import lombok.RequiredArgsConstructor;
-import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import com.google.common.io.CharStreams;
+import java.io.PrintWriter;
+import java.io.Writer;
 
-@RequiredArgsConstructor
-public class RetrieveAllNonClosedLoanIdServiceImpl implements 
RetrieveLoanIdService {
+public final class ThrowableSerialization {
 
-    private final LoanRepository loanRepository;
+    private ThrowableSerialization() {
 
-    @Override
-    public List<Integer> retrieveLoanIds() {
-        return loanRepository.findAllNonClosedLoanIds();
+    }
+
+    @SuppressWarnings("RegexpSinglelineJava")
+    public static String serialize(Throwable e) {
+        StringBuilder sb = new StringBuilder();
+        Writer w = CharStreams.asWriter(sb);
+        e.printStackTrace(new PrintWriter(w, true));
+        return sb.toString();
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBMessage.java 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/ContextualMessage.java
similarity index 90%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBMessage.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/ContextualMessage.java
index f7b04ea7f..0cb1988e0 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/COBMessage.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/ContextualMessage.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.infrastructure.springbatch;
 
 import java.io.Serializable;
 import lombok.Data;
@@ -24,7 +24,7 @@ import 
org.apache.fineract.infrastructure.core.domain.FineractContext;
 import org.springframework.batch.integration.partition.StepExecutionRequest;
 
 @Data
-public class COBMessage implements Serializable {
+public class ContextualMessage implements Serializable {
 
     private StepExecutionRequest stepExecutionRequest;
     private FineractContext context;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBInputChannelInterceptor.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/InputChannelInterceptor.java
similarity index 79%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBInputChannelInterceptor.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/InputChannelInterceptor.java
index cb8e4b026..e3e71a2b9 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBInputChannelInterceptor.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/InputChannelInterceptor.java
@@ -16,21 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.infrastructure.springbatch;
 
 import org.apache.fineract.infrastructure.core.domain.ActionContext;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.messaging.Message;
 import org.springframework.messaging.MessageChannel;
 import org.springframework.messaging.MessageHandler;
 import org.springframework.messaging.support.ExecutorChannelInterceptor;
 import org.springframework.messaging.support.GenericMessage;
 
-public class COBInputChannelInterceptor implements ExecutorChannelInterceptor {
+public class InputChannelInterceptor implements ExecutorChannelInterceptor {
 
     @Override
-    public Message<?> beforeHandle(Message<?> message, MessageChannel channel, 
MessageHandler handler) {
-        COBMessage castedMessage = COBMessage.class.cast(message.getPayload());
+    public Message<?> beforeHandle(Message<?> message, @NotNull MessageChannel 
channel, @NotNull MessageHandler handler) {
+        ContextualMessage castedMessage = (ContextualMessage) 
message.getPayload();
         ThreadLocalContextUtil.init(castedMessage.getContext());
         ThreadLocalContextUtil.setActionContext(ActionContext.COB);
         return new GenericMessage<>(castedMessage.getStepExecutionRequest());
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBManagerConfig.java 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/ManagerConfig.java
similarity index 88%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBManagerConfig.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/ManagerConfig.java
index 567b38b2e..f25f69f0a 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBManagerConfig.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/ManagerConfig.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.infrastructure.springbatch;
 
 import 
org.springframework.batch.integration.config.annotation.EnableBatchIntegration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -27,7 +27,7 @@ import org.springframework.integration.channel.DirectChannel;
 @Configuration
 @EnableBatchIntegration
 @ConditionalOnProperty(value = "fineract.mode.batch-manager-enabled", 
havingValue = "true")
-public class COBManagerConfig {
+public class ManagerConfig {
 
     @Bean
     public DirectChannel outboundRequests() {
@@ -35,7 +35,7 @@ public class COBManagerConfig {
     }
 
     @Bean
-    public COBOutputChannelInterceptor outputInterceptor() {
-        return new COBOutputChannelInterceptor();
+    public OutputChannelInterceptor outputInterceptor() {
+        return new OutputChannelInterceptor();
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBOutputChannelInterceptor.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/OutputChannelInterceptor.java
similarity index 66%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBOutputChannelInterceptor.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/OutputChannelInterceptor.java
index 5c06c4d94..323c00510 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBOutputChannelInterceptor.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/OutputChannelInterceptor.java
@@ -16,23 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.infrastructure.springbatch;
 
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.jetbrains.annotations.NotNull;
 import org.springframework.batch.integration.async.StepExecutionInterceptor;
 import org.springframework.batch.integration.partition.StepExecutionRequest;
 import org.springframework.messaging.Message;
 import org.springframework.messaging.MessageChannel;
 import org.springframework.messaging.support.GenericMessage;
 
-public class COBOutputChannelInterceptor extends StepExecutionInterceptor {
+public class OutputChannelInterceptor extends StepExecutionInterceptor {
 
     @Override
-    public Message<?> preSend(Message<?> message, MessageChannel channel) {
-        StepExecutionRequest stepExecutionRequest = 
StepExecutionRequest.class.cast(message.getPayload());
-        COBMessage cobMessage = new COBMessage();
-        cobMessage.setStepExecutionRequest(stepExecutionRequest);
-        cobMessage.setContext(ThreadLocalContextUtil.getContext());
-        return new GenericMessage<>(cobMessage);
+    public Message<?> preSend(Message<?> message, @NotNull MessageChannel 
channel) {
+        StepExecutionRequest stepExecutionRequest = (StepExecutionRequest) 
message.getPayload();
+        ContextualMessage contextualMessage = new ContextualMessage();
+        contextualMessage.setStepExecutionRequest(stepExecutionRequest);
+        contextualMessage.setContext(ThreadLocalContextUtil.getContext());
+        return new GenericMessage<>(contextualMessage);
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java
similarity index 90%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java
index 58c6d534c..7a9ba0294 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyService.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyService.java
@@ -16,9 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.infrastructure.springbatch;
 
-public interface COBPropertyService {
+public interface PropertyService {
 
     Integer getPartitionSize(String jobName);
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyServiceImpl.java
similarity index 92%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyServiceImpl.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyServiceImpl.java
index 27f3d5a8e..1d4e9d88c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBPropertyServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/PropertyServiceImpl.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.infrastructure.springbatch;
 
 import java.util.List;
 import lombok.RequiredArgsConstructor;
@@ -25,7 +25,7 @@ import org.springframework.stereotype.Service;
 
 @Service
 @RequiredArgsConstructor
-public class COBPropertyServiceImpl implements COBPropertyService {
+public class PropertyServiceImpl implements PropertyService {
 
     private final FineractProperties fineractProperties;
 
@@ -37,7 +37,7 @@ public class COBPropertyServiceImpl implements 
COBPropertyService {
                 .filter(jobProperty -> 
jobName.equals(jobProperty.getJobName())) //
                 .findFirst() //
                 
.map(FineractProperties.PartitionedJobProperty::getPartitionSize) //
-                .orElse(0);
+                .orElse(1);
     }
 
     @Override
@@ -48,6 +48,6 @@ public class COBPropertyServiceImpl implements 
COBPropertyService {
                 .filter(jobProperty -> 
jobName.equals(jobProperty.getJobName())) //
                 .findFirst() //
                 .map(FineractProperties.PartitionedJobProperty::getChunkSize) 
//
-                .orElse(0);
+                .orElse(1);
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBWorkerConfig.java 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/WorkerConfig.java
similarity index 87%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/COBWorkerConfig.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/WorkerConfig.java
index 5cb42e456..4ffaf36e3 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/COBWorkerConfig.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/WorkerConfig.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob;
+package org.apache.fineract.infrastructure.springbatch;
 
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
@@ -25,7 +25,7 @@ import org.springframework.integration.channel.QueueChannel;
 
 @Configuration
 @ConditionalOnProperty(value = "fineract.mode.batch-worker-enabled", 
havingValue = "true")
-public class COBWorkerConfig {
+public class WorkerConfig {
 
     @Bean
     public QueueChannel inboundRequests() {
@@ -33,7 +33,7 @@ public class COBWorkerConfig {
     }
 
     @Bean
-    public COBInputChannelInterceptor inputInterceptor() {
-        return new COBInputChannelInterceptor();
+    public InputChannelInterceptor inputInterceptor() {
+        return new InputChannelInterceptor();
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/JmsBrokerConfiguration.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsBrokerConfiguration.java
similarity index 96%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/JmsBrokerConfiguration.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsBrokerConfiguration.java
index bf776fcd1..96df69941 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/JmsBrokerConfiguration.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsBrokerConfiguration.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler;
+package org.apache.fineract.infrastructure.springbatch.messagehandler;
 
 import org.apache.activemq.ActiveMQConnectionFactory;
 import org.apache.fineract.infrastructure.core.config.FineractProperties;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBJmsManagerConfig.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsManagerConfig.java
similarity index 87%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBJmsManagerConfig.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsManagerConfig.java
index efe3a3588..a0bd8c384 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBJmsManagerConfig.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsManagerConfig.java
@@ -16,12 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler;
+package org.apache.fineract.infrastructure.springbatch.messagehandler;
 
 import org.apache.activemq.ActiveMQConnectionFactory;
-import org.apache.fineract.cob.COBOutputChannelInterceptor;
-import org.apache.fineract.cob.messagehandler.conditions.JmsManagerCondition;
 import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.springbatch.OutputChannelInterceptor;
+import 
org.apache.fineract.infrastructure.springbatch.messagehandler.conditions.JmsManagerCondition;
 import 
org.springframework.batch.integration.config.annotation.EnableBatchIntegration;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
@@ -38,12 +38,12 @@ import org.springframework.integration.jms.dsl.Jms;
 @EnableBatchIntegration
 @Conditional(JmsManagerCondition.class)
 @Import(value = { JmsBrokerConfiguration.class })
-public class COBJmsManagerConfig {
+public class JmsManagerConfig {
 
     @Autowired
     private DirectChannel outboundRequests;
     @Autowired
-    private COBOutputChannelInterceptor outputInterceptor;
+    private OutputChannelInterceptor outputInterceptor;
     @Autowired
     private FineractProperties fineractProperties;
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBJmsWorkerConfig.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsWorkerConfig.java
similarity index 87%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBJmsWorkerConfig.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsWorkerConfig.java
index cba4ff323..3571718f7 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBJmsWorkerConfig.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/JmsWorkerConfig.java
@@ -16,12 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler;
+package org.apache.fineract.infrastructure.springbatch.messagehandler;
 
 import org.apache.activemq.ActiveMQConnectionFactory;
-import org.apache.fineract.cob.COBInputChannelInterceptor;
-import org.apache.fineract.cob.messagehandler.conditions.JmsWorkerCondition;
 import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.springbatch.InputChannelInterceptor;
+import 
org.apache.fineract.infrastructure.springbatch.messagehandler.conditions.JmsWorkerCondition;
 import 
org.springframework.batch.integration.config.annotation.EnableBatchIntegration;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
@@ -38,12 +38,12 @@ import org.springframework.integration.jms.dsl.Jms;
 @EnableBatchIntegration
 @Conditional(JmsWorkerCondition.class)
 @Import(value = { JmsBrokerConfiguration.class })
-public class COBJmsWorkerConfig {
+public class JmsWorkerConfig {
 
     @Autowired
     private QueueChannel inboundRequests;
     @Autowired
-    private COBInputChannelInterceptor inputInterceptor;
+    private InputChannelInterceptor inputInterceptor;
     @Autowired
     private FineractProperties fineractProperties;
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBSpringEventManagerConfig.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/SpringEventManagerConfig.java
similarity index 85%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBSpringEventManagerConfig.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/SpringEventManagerConfig.java
index e748fe628..42cdd35c1 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBSpringEventManagerConfig.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/SpringEventManagerConfig.java
@@ -16,10 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler;
+package org.apache.fineract.infrastructure.springbatch.messagehandler;
 
-import org.apache.fineract.cob.COBOutputChannelInterceptor;
-import 
org.apache.fineract.cob.messagehandler.conditions.SpringEventManagerCondition;
+import org.apache.fineract.infrastructure.springbatch.OutputChannelInterceptor;
+import 
org.apache.fineract.infrastructure.springbatch.messagehandler.conditions.SpringEventManagerCondition;
 import 
org.springframework.batch.integration.config.annotation.EnableBatchIntegration;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
@@ -34,12 +34,12 @@ import 
org.springframework.integration.handler.LoggingHandler;
 @Configuration
 @EnableBatchIntegration
 @Conditional(SpringEventManagerCondition.class)
-public class COBSpringEventManagerConfig {
+public class SpringEventManagerConfig {
 
     @Autowired
     private DirectChannel outboundRequests;
     @Autowired
-    private COBOutputChannelInterceptor outputInterceptor;
+    private OutputChannelInterceptor outputInterceptor;
 
     @Bean
     public IntegrationFlow outboundFlow() {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBSpringEventWorkerConfig.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/SpringEventWorkerConfig.java
similarity index 86%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBSpringEventWorkerConfig.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/SpringEventWorkerConfig.java
index 8ebcf5037..7691e24f0 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/COBSpringEventWorkerConfig.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/SpringEventWorkerConfig.java
@@ -16,10 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler;
+package org.apache.fineract.infrastructure.springbatch.messagehandler;
 
-import org.apache.fineract.cob.COBInputChannelInterceptor;
-import 
org.apache.fineract.cob.messagehandler.conditions.SpringEventWorkerCondition;
+import org.apache.fineract.infrastructure.springbatch.InputChannelInterceptor;
+import 
org.apache.fineract.infrastructure.springbatch.messagehandler.conditions.SpringEventWorkerCondition;
 import 
org.springframework.batch.integration.config.annotation.EnableBatchIntegration;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
@@ -35,12 +35,12 @@ import 
org.springframework.integration.handler.LoggingHandler;
 @Configuration
 @EnableBatchIntegration
 @Conditional(SpringEventWorkerCondition.class)
-public class COBSpringEventWorkerConfig {
+public class SpringEventWorkerConfig {
 
     @Autowired
     private QueueChannel inboundRequests;
     @Autowired
-    private COBInputChannelInterceptor inputInterceptor;
+    private InputChannelInterceptor inputInterceptor;
 
     @Bean
     public IntegrationFlow inboundFlow() {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/JmsManagerCondition.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/JmsManagerCondition.java
similarity index 94%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/JmsManagerCondition.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/JmsManagerCondition.java
index 30c4e3ed8..b8bea3104 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/JmsManagerCondition.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/JmsManagerCondition.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler.conditions;
+package 
org.apache.fineract.infrastructure.springbatch.messagehandler.conditions;
 
 import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/JmsWorkerCondition.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/JmsWorkerCondition.java
similarity index 94%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/JmsWorkerCondition.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/JmsWorkerCondition.java
index 838fe3adf..7bdeeca5b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/JmsWorkerCondition.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/JmsWorkerCondition.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler.conditions;
+package 
org.apache.fineract.infrastructure.springbatch.messagehandler.conditions;
 
 import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/SpringEventManagerCondition.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/SpringEventManagerCondition.java
similarity index 94%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/SpringEventManagerCondition.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/SpringEventManagerCondition.java
index 2afe872e2..b0fe92071 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/SpringEventManagerCondition.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/SpringEventManagerCondition.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler.conditions;
+package 
org.apache.fineract.infrastructure.springbatch.messagehandler.conditions;
 
 import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/SpringEventWorkerCondition.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/SpringEventWorkerCondition.java
similarity index 94%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/SpringEventWorkerCondition.java
rename to 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/SpringEventWorkerCondition.java
index c11ec5842..38f243d89 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/messagehandler/conditions/SpringEventWorkerCondition.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/springbatch/messagehandler/conditions/SpringEventWorkerCondition.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.messagehandler.conditions;
+package 
org.apache.fineract.infrastructure.springbatch.messagehandler.conditions;
 
 import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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 f75996e70..5e82c1e06 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
@@ -166,6 +166,6 @@ public interface LoanRepository extends JpaRepository<Loan, 
Long>, JpaSpecificat
     boolean existsByExternalId(@Param("externalId") String externalId);
 
     @Query(FIND_ALL_NON_CLOSED)
-    List<Integer> findAllNonClosedLoanIds();
+    List<Long> findAllNonClosedLoanIds();
 
 }
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index d79f96d5c..fe802d965 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -67,4 +67,5 @@
     <include file="parts/0045_external_event_table_data_binary.xml" 
relativeToChangelogFile="true"/>
     <include file="parts/0046_external_event_table_schema_info.xml" 
relativeToChangelogFile="true"/>
     <include file="parts/0047_add_loan_delinquency_tags_business_step.xml" 
relativeToChangelogFile="true"/>
+    <include file="parts/0048_rework_loan_account_locks.xml" 
relativeToChangelogFile="true"/>
 </databaseChangeLog>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0048_rework_loan_account_locks.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0048_rework_loan_account_locks.xml
new file mode 100644
index 000000000..b8540b205
--- /dev/null
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0048_rework_loan_account_locks.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog";
+                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                   
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd";>
+
+    <changeSet id="1" author="fineract">
+        <dropColumn tableName="m_loan_account_locks" 
columnName="lock_placed_on"/>
+        <dropColumn tableName="m_loan_account_locks" columnName="stacktrace"/>
+        <addColumn tableName="m_loan_account_locks">
+            <column name="stacktrace" type="TEXT"/>
+        </addColumn>
+    </changeSet>
+
+    <changeSet author="fineract" id="10" context="mysql">
+        <addColumn tableName="m_loan_account_locks">
+            <column name="lock_placed_on" type="DATETIME">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+    </changeSet>
+    <changeSet author="fineract" id="10" context="postgresql">
+        <addColumn tableName="m_loan_account_locks">
+            <column name="lock_placed_on" type="TIMESTAMP WITH TIME ZONE">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+    </changeSet>
+</databaseChangeLog>
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java
 
b/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java
new file mode 100644
index 000000000..a82a8a110
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/COBBusinessStepServiceStepDefinitions.java
@@ -0,0 +1,167 @@
+/**
+ * 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.cob;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+
+import com.google.common.base.Splitter;
+import io.cucumber.java8.En;
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.TreeMap;
+import org.apache.fineract.cob.domain.BatchBusinessStep;
+import org.apache.fineract.cob.domain.BatchBusinessStepRepository;
+import org.apache.fineract.cob.exceptions.BusinessStepException;
+import org.apache.fineract.cob.loan.LoanCOBBusinessStep;
+import org.apache.fineract.infrastructure.core.domain.AbstractAuditableCustom;
+import org.apache.fineract.infrastructure.core.domain.ActionContext;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.mix.data.MixTaxonomyData;
+import org.springframework.beans.factory.BeanCreationException;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+
+public class COBBusinessStepServiceStepDefinitions implements En {
+
+    private ApplicationContext applicationContext = 
mock(ApplicationContext.class);
+    private ListableBeanFactory beanFactory = mock(ListableBeanFactory.class);
+    private BatchBusinessStepRepository batchBusinessStepRepository = 
mock(BatchBusinessStepRepository.class);
+    private final COBBusinessStepService businessStepService = new 
COBBusinessStepServiceImpl(batchBusinessStepRepository,
+            applicationContext, beanFactory);
+    private COBBusinessStep cobBusinessStep = mock(COBBusinessStep.class);
+    private COBBusinessStep notRegistereCobBusinessStep = 
mock(COBBusinessStep.class);
+    private TreeMap<Long, String> executionMap;
+    private AbstractAuditableCustom item;
+    private AbstractAuditableCustom outputItem = 
mock(AbstractAuditableCustom.class);
+
+    private AbstractAuditableCustom resultItem;
+
+    private HashMap<MixTaxonomyData, BigDecimal> data = new HashMap<>();
+
+    private String result;
+    private Class clazz;
+    private String jobName;
+    private BatchBusinessStep batchBusinessStep = 
mock(BatchBusinessStep.class);
+    private TreeMap<Long, String> resultMap;
+
+    public COBBusinessStepServiceStepDefinitions() {
+        Given("/^The COBBusinessStepService.run method with executeMap 
(.*)$/", (String executionMap) -> {
+            if ("null".equals(executionMap)) {
+                this.executionMap = null;
+            } else if ("".equals(executionMap)) {
+                this.executionMap = new TreeMap<>();
+            } else {
+                List<String> splitStr = 
Splitter.on(',').splitToList(executionMap);
+                Long key = Long.parseLong(splitStr.get(0));
+                String value = splitStr.get(1);
+                this.executionMap = new TreeMap<>();
+                this.executionMap.put(key, value);
+            }
+
+            this.item = mock(AbstractAuditableCustom.class);
+
+            
lenient().when(this.applicationContext.getBean("test")).thenReturn(cobBusinessStep);
+            
lenient().when(this.applicationContext.getBean("notExist")).thenThrow(BeanCreationException.class);
+            
lenient().when(this.cobBusinessStep.execute(this.item)).thenReturn(outputItem);
+
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        });
+
+        Given("/^The COBBusinessStepService.getCOBBusinessStepMap method with 
businessStepClass (.*), and jobName (.*)$/",
+                (String className, String jobName) -> {
+                    if ("null".equals(className)) {
+                        this.clazz = null;
+                    } else if ("empty".equals(className)) {
+                        this.clazz = String.class;
+                    } else if ("LoanCOBBusinessStep".equals(className)) {
+                        this.clazz = LoanCOBBusinessStep.class;
+                    } else {
+                        this.clazz = Object.class;
+                    }
+
+                    this.jobName = jobName;
+
+                    
lenient().when(this.batchBusinessStepRepository.findAllByJobName("exist"))
+                            
.thenReturn(Collections.singletonList(this.batchBusinessStep));
+                    
lenient().when(this.batchBusinessStepRepository.findAllByJobName("notExist")).thenReturn(Collections.emptyList());
+
+                    
lenient().when(this.beanFactory.getBeanNamesForType((Class<?>) 
null)).thenReturn(new String[] { "notExist" });
+                    
lenient().when(this.beanFactory.getBeanNamesForType(Object.class)).thenReturn(new
 String[] { "testNotRegistered" });
+                    
lenient().when(this.beanFactory.getBeanNamesForType(String.class)).thenReturn(new
 String[] {});
+                    
lenient().when(this.beanFactory.getBeanNamesForType(LoanCOBBusinessStep.class)).thenReturn(new
 String[] { "test" });
+
+                    
lenient().when(this.applicationContext.getBean("test")).thenReturn(this.cobBusinessStep);
+                    
lenient().when(this.applicationContext.getBean("testNotRegistered")).thenReturn(this.notRegistereCobBusinessStep);
+                    
lenient().when(this.applicationContext.getBean("notExist")).thenThrow(BeanCreationException.class);
+
+                    
lenient().when(this.cobBusinessStep.getEnumStyledName()).thenReturn("registered");
+                    
lenient().when(this.notRegistereCobBusinessStep.getEnumStyledName()).thenReturn("notRegistered");
+                    
lenient().when(this.batchBusinessStep.getStepName()).thenReturn("registered");
+                    
lenient().when(this.batchBusinessStep.getStepOrder()).thenReturn(1L);
+
+                    
lenient().when(this.cobBusinessStep.execute(this.item)).thenReturn(outputItem);
+
+                });
+
+        When("COBBusinessStepService.run method executed", () -> {
+            resultItem = this.businessStepService.run(this.executionMap, 
this.item);
+        });
+
+        When("COBBusinessStepService.getCOBBusinessStepMap method executed", 
() -> {
+            resultMap = 
this.businessStepService.getCOBBusinessStepMap(this.clazz, this.jobName);
+        });
+
+        Then("The COBBusinessStepService.run result should match", () -> {
+            assertEquals(outputItem, resultItem);
+            assertEquals(ActionContext.COB, 
ThreadLocalContextUtil.getActionContext());
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        });
+
+        Then("throw exception COBBusinessStepService.run method", () -> {
+            assertThrows(BusinessStepException.class, () -> {
+                resultItem = this.businessStepService.run(this.executionMap, 
this.item);
+            });
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        });
+
+        Then("The COBBusinessStepService.getCOBBusinessStepMap result 
exception", () -> {
+            assertThrows(BeanCreationException.class, () -> {
+                this.businessStepService.getCOBBusinessStepMap(this.clazz, 
this.jobName);
+            });
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        });
+
+        Then("The COBBusinessStepService.getCOBBusinessStepMap result should 
match", () -> {
+            assertEquals(1, resultMap.size());
+            assertEquals("test", resultMap.get(1L));
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        });
+
+        Then("The COBBusinessStepService.getCOBBusinessStepMap result empty", 
() -> {
+            assertEquals(0, resultMap.size());
+            ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        });
+
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java
 
b/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java
new file mode 100644
index 000000000..435266e52
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/common/InitialisationTaskletStepDefinitions.java
@@ -0,0 +1,67 @@
+/**
+ * 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.cob.common;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+
+import io.cucumber.java8.En;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
+import org.springframework.batch.repeat.RepeatStatus;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+public class InitialisationTaskletStepDefinitions implements En {
+
+    private AppUserRepositoryWrapper userRepository = 
mock(AppUserRepositoryWrapper.class);
+
+    private InitialisationTasklet initialisationTasklet = new 
InitialisationTasklet(userRepository);
+
+    private AppUser appUser = mock(AppUser.class);
+    private RepeatStatus resultItem;
+
+    public InitialisationTaskletStepDefinitions() {
+        Given("/^The InitialisationTasklet.execute method with action (.*)$/", 
(String action) -> {
+
+            if ("error".equals(action)) {
+                
lenient().when(this.userRepository.fetchSystemUser()).thenThrow(new 
RuntimeException("fail"));
+            } else {
+                
lenient().when(this.userRepository.fetchSystemUser()).thenReturn(appUser);
+            }
+
+        });
+
+        When("InitialisationTasklet.execute method executed", () -> {
+            resultItem = this.initialisationTasklet.execute(null, null);
+        });
+
+        Then("InitialisationTasklet.execute result should match", () -> {
+            assertEquals(RepeatStatus.FINISHED, resultItem);
+            assertEquals(appUser, 
SecurityContextHolder.getContext().getAuthentication().getPrincipal());
+        });
+
+        Then("throw exception InitialisationTasklet.execute method", () -> {
+            assertThrows(RuntimeException.class, () -> {
+                resultItem = this.initialisationTasklet.execute(null, null);
+            });
+        });
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/cob/listener/LoanItemListenerStepDefinitions.java
 
b/fineract-provider/src/test/java/org/apache/fineract/cob/listener/LoanItemListenerStepDefinitions.java
new file mode 100644
index 000000000..757669ff2
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/listener/LoanItemListenerStepDefinitions.java
@@ -0,0 +1,123 @@
+/**
+ * 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.cob.listener;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.cucumber.java8.En;
+import java.util.List;
+import java.util.Optional;
+import org.apache.fineract.cob.domain.LoanAccountLock;
+import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.cob.exceptions.LoanReadException;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.mockito.Mockito;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.support.TransactionTemplate;
+
+public class LoanItemListenerStepDefinitions implements En {
+
+    private LoanAccountLockRepository accountLockRepository = 
mock(LoanAccountLockRepository.class);
+    private TransactionTemplate transactionTemplate = 
spy(TransactionTemplate.class);
+
+    private LoanItemListener loanItemListener = new 
LoanItemListener(accountLockRepository, transactionTemplate);
+
+    private Exception exception;
+
+    private LoanAccountLock loanAccountLock;
+    private Loan loan = mock(Loan.class);
+
+    public LoanItemListenerStepDefinitions() {
+        Given("/^The LoanItemListener.onReadError method (.*)$/", (String 
action) -> {
+            ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, 
"default", "Default", "Asia/Kolkata", null));
+            exception = new LoanReadException(1L, new 
RuntimeException("fail"));
+            loanAccountLock = new LoanAccountLock(1L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+            when(accountLockRepository.findByLoanIdAndLockOwner(1L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING))
+                    .thenReturn(Optional.of(loanAccountLock));
+            
transactionTemplate.setTransactionManager(mock(PlatformTransactionManager.class));
+            when(loan.getId()).thenReturn(1L);
+        });
+
+        When("LoanItemListener.onReadError method executed", () -> {
+            this.loanItemListener.onReadError(exception);
+        });
+
+        Then("LoanItemListener.onReadError result should match", () -> {
+            verify(transactionTemplate, 
Mockito.times(1)).setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+            verify(transactionTemplate, Mockito.times(1)).execute(any());
+            verify(accountLockRepository, 
Mockito.times(1)).findByLoanIdAndLockOwner(1L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+            assertEquals("Loan (id: 1) reading is failed", 
loanAccountLock.getError());
+            assertNotNull(loanAccountLock.getStacktrace());
+        });
+
+        Given("/^The LoanItemListener.onProcessError method (.*)$/", (String 
action) -> {
+            ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, 
"default", "Default", "Asia/Kolkata", null));
+            exception = new LoanReadException(1L, new 
RuntimeException("fail"));
+            loanAccountLock = new LoanAccountLock(2L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+            when(accountLockRepository.findByLoanIdAndLockOwner(2L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING))
+                    .thenReturn(Optional.of(loanAccountLock));
+            when(loan.getId()).thenReturn(2L);
+            
transactionTemplate.setTransactionManager(mock(PlatformTransactionManager.class));
+        });
+
+        When("LoanItemListener.onProcessError method executed", () -> {
+            this.loanItemListener.onProcessError(loan, exception);
+        });
+
+        Then("LoanItemListener.onProcessError result should match", () -> {
+            verify(transactionTemplate, 
Mockito.times(1)).setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+            verify(transactionTemplate, Mockito.times(1)).execute(any());
+            verify(accountLockRepository, 
Mockito.times(1)).findByLoanIdAndLockOwner(2L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+            assertEquals("Loan (id: 2) processing is failed", 
loanAccountLock.getError());
+            assertNotNull(loanAccountLock.getStacktrace());
+        });
+
+        Given("/^The LoanItemListener.onWriteError method (.*)$/", (String 
action) -> {
+            ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, 
"default", "Default", "Asia/Kolkata", null));
+            exception = new LoanReadException(3L, new 
RuntimeException("fail"));
+            loanAccountLock = new LoanAccountLock(3L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+            when(accountLockRepository.findByLoanIdAndLockOwner(3L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING))
+                    .thenReturn(Optional.of(loanAccountLock));
+            when(loan.getId()).thenReturn(3L);
+            
transactionTemplate.setTransactionManager(mock(PlatformTransactionManager.class));
+        });
+
+        When("LoanItemListener.onWriteError method executed", () -> {
+            this.loanItemListener.onWriteError(exception, List.of(loan));
+        });
+
+        Then("LoanItemListener.onWriteError result should match", () -> {
+            verify(transactionTemplate, 
Mockito.times(1)).setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+            verify(transactionTemplate, Mockito.times(1)).execute(any());
+            verify(accountLockRepository, 
Mockito.times(1)).findByLoanIdAndLockOwner(3L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+            assertEquals("Loan (id: 3) writing is failed", 
loanAccountLock.getError());
+            assertNotNull(loanAccountLock.getStacktrace());
+        });
+    }
+}
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
new file mode 100644
index 000000000..3b5ffa113
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/ApplyLoanLockTaskletStepDefinitions.java
@@ -0,0 +1,92 @@
+/**
+ * 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.cob.loan;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.cucumber.java8.En;
+import java.util.List;
+import org.apache.fineract.cob.domain.LoanAccountLock;
+import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.springframework.batch.core.StepContribution;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.repeat.RepeatStatus;
+
+public class ApplyLoanLockTaskletStepDefinitions implements En {
+
+    ArgumentCaptor<LoanAccountLock> valueCaptor = 
ArgumentCaptor.forClass(LoanAccountLock.class);
+    private LoanAccountLockRepository accountLockRepository = 
mock(LoanAccountLockRepository.class);
+    private ApplyLoanLockTasklet applyLoanLockTasklet = new 
ApplyLoanLockTasklet(accountLockRepository);
+    private RepeatStatus resultItem;
+    private StepContribution stepContribution;
+
+    public ApplyLoanLockTaskletStepDefinitions() {
+        Given("/^The ApplyLoanLockTasklet.execute method with action (.*)$/", 
(String action) -> {
+            ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, 
"default", "Default", "Asia/Kolkata", null));
+            StepExecution stepExecution = new StepExecution("test", null);
+            ExecutionContext executionContext = new ExecutionContext();
+            executionContext.put(LoanCOBConstant.LOAN_IDS, List.of(1L, 2L, 3L, 
4L));
+            stepExecution.setExecutionContext(executionContext);
+            this.stepContribution = new StepContribution(stepExecution);
+
+            if ("error".equals(action)) {
+                
lenient().when(this.accountLockRepository.findAllByLoanIdIn(Mockito.anyList())).thenThrow(new
 RuntimeException("fail"));
+            } else {
+                LoanAccountLock lock1 = new LoanAccountLock(1L, 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+                LoanAccountLock lock2 = new LoanAccountLock(2L, 
LockOwner.LOAN_COB_PARTITIONING);
+                LoanAccountLock lock3 = new LoanAccountLock(3L, 
LockOwner.LOAN_INLINE_COB_PROCESSING);
+                List<LoanAccountLock> accountLocks = List.of(lock1, lock2, 
lock3);
+                
lenient().when(this.accountLockRepository.findAllByLoanIdIn(Mockito.anyList())).thenReturn(accountLocks);
+            }
+
+        });
+
+        When("ApplyLoanLockTasklet.execute method executed", () -> {
+            resultItem = this.applyLoanLockTasklet.execute(stepContribution, 
null);
+        });
+
+        Then("ApplyLoanLockTasklet.execute result should match", () -> {
+            assertEquals(RepeatStatus.FINISHED, resultItem);
+            assertEquals(3L, ((List) 
stepContribution.getStepExecution().getExecutionContext()
+                    
.get(LoanCOBWorkerConfiguration.ALREADY_LOCKED_LOAN_IDS)).get(0));
+            verify(this.accountLockRepository, 
Mockito.times(2)).save(valueCaptor.capture());
+            List<LoanAccountLock> values = valueCaptor.getAllValues();
+            assertEquals(2L, values.get(0).getLoanId());
+            assertEquals(LockOwner.LOAN_COB_CHUNK_PROCESSING, 
values.get(0).getLockOwner());
+            assertEquals(4L, values.get(1).getLoanId());
+            assertEquals(LockOwner.LOAN_COB_CHUNK_PROCESSING, 
values.get(1).getLockOwner());
+        });
+
+        Then("throw exception ApplyLoanLockTasklet.execute method", () -> {
+            assertThrows(RuntimeException.class, () -> {
+                resultItem = 
this.applyLoanLockTasklet.execute(stepContribution, null);
+            });
+        });
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanCOBPartitionerStepDefinitions.java
 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanCOBPartitionerStepDefinitions.java
new file mode 100644
index 000000000..926564c96
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanCOBPartitionerStepDefinitions.java
@@ -0,0 +1,113 @@
+/**
+ * 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.cob.loan;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.cucumber.java8.En;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import org.apache.fineract.cob.COBBusinessStepService;
+import org.apache.fineract.infrastructure.jobs.service.JobName;
+import org.apache.fineract.infrastructure.springbatch.PropertyService;
+import org.mockito.Mockito;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.launch.JobOperator;
+import org.springframework.batch.item.ExecutionContext;
+
+public class LoanCOBPartitionerStepDefinitions implements En {
+
+    PropertyService propertyService = mock(PropertyService.class);
+    COBBusinessStepService cobBusinessStepService = 
mock(COBBusinessStepService.class);
+    JobOperator jobOperator = mock(JobOperator.class);
+    JobExplorer jobExplorer = mock(JobExplorer.class);
+    RetrieveLoanIdService retrieveLoanIdService = 
mock(RetrieveLoanIdService.class);
+    private LoanCOBPartitioner loanCOBPartitioner = new 
LoanCOBPartitioner(propertyService, cobBusinessStepService, jobOperator,
+            jobExplorer, retrieveLoanIdService);
+
+    private TreeMap<Long, String> cobBusinessMap = new TreeMap<>();
+
+    private Map<String, ExecutionContext> resultItem;
+    private String action;
+
+    public LoanCOBPartitionerStepDefinitions() {
+        Given("/^The LoanCOBPartitioner.partition method with action (.*)$/", 
(String action) -> {
+
+            this.action = action;
+            
lenient().when(propertyService.getPartitionSize(LoanCOBConstant.JOB_NAME)).thenReturn(2);
+            if ("empty steps".equals(action)) {
+                
lenient().when(cobBusinessStepService.getCOBBusinessStepMap(LoanCOBBusinessStep.class,
 LoanCOBConstant.LOAN_COB_JOB_NAME))
+                        .thenReturn(new TreeMap<>());
+                
lenient().when(jobExplorer.findRunningJobExecutions(JobName.LOAN_COB.name())).thenReturn(Set.of(new
 JobExecution(3L)));
+                lenient().when(jobOperator.stop(3L)).thenReturn(Boolean.TRUE);
+            } else if ("empty loanIds".equals(action)) {
+                cobBusinessMap.put(1L, "Business step");
+                
lenient().when(cobBusinessStepService.getCOBBusinessStepMap(LoanCOBBusinessStep.class,
 LoanCOBConstant.LOAN_COB_JOB_NAME))
+                        .thenReturn(cobBusinessMap);
+                
lenient().when(retrieveLoanIdService.retrieveLoanIds()).thenReturn(Collections.emptyList());
+                
lenient().when(jobExplorer.findRunningJobExecutions(JobName.LOAN_COB.name())).thenThrow(new
 RuntimeException("fail"));
+            } else if ("good".equals(action)) {
+                cobBusinessMap.put(1L, "Business step");
+                
lenient().when(cobBusinessStepService.getCOBBusinessStepMap(LoanCOBBusinessStep.class,
 LoanCOBConstant.LOAN_COB_JOB_NAME))
+                        .thenReturn(cobBusinessMap);
+                
lenient().when(retrieveLoanIdService.retrieveLoanIds()).thenReturn(List.of(1L, 
2L, 3L));
+            }
+        });
+
+        When("LoanCOBPartitioner.partition method executed", () -> {
+            resultItem = this.loanCOBPartitioner.partition(2);
+        });
+
+        Then("LoanCOBPartitioner.partition result should match", () -> {
+            if ("empty steps".equals(action)) {
+                verify(jobOperator, Mockito.times(1)).stop(3L);
+                assertTrue(resultItem.isEmpty());
+            } else if ("good".equals(action)) {
+                verify(jobOperator, Mockito.times(0)).stop(Mockito.anyLong());
+                assertEquals(2, resultItem.size());
+                
assertTrue(resultItem.containsKey(LoanCOBPartitioner.PARTITION_PREFIX + "1"));
+                assertEquals(cobBusinessMap,
+                        resultItem.get(LoanCOBPartitioner.PARTITION_PREFIX + 
"1").get(LoanCOBConstant.BUSINESS_STEP_MAP));
+                assertEquals(2, ((List) 
resultItem.get(LoanCOBPartitioner.PARTITION_PREFIX + 
"1").get(LoanCOBConstant.LOAN_IDS)).size());
+                assertEquals(1L, ((List) 
resultItem.get(LoanCOBPartitioner.PARTITION_PREFIX + 
"1").get(LoanCOBConstant.LOAN_IDS)).get(0));
+                assertEquals(2L, ((List) 
resultItem.get(LoanCOBPartitioner.PARTITION_PREFIX + 
"1").get(LoanCOBConstant.LOAN_IDS)).get(1));
+                
assertTrue(resultItem.containsKey(LoanCOBPartitioner.PARTITION_PREFIX + "2"));
+                assertEquals(cobBusinessMap,
+                        resultItem.get(LoanCOBPartitioner.PARTITION_PREFIX + 
"2").get(LoanCOBConstant.BUSINESS_STEP_MAP));
+                assertEquals(1, ((List) 
resultItem.get(LoanCOBPartitioner.PARTITION_PREFIX + 
"2").get(LoanCOBConstant.LOAN_IDS)).size());
+                assertEquals(3L, ((List) 
resultItem.get(LoanCOBPartitioner.PARTITION_PREFIX + 
"2").get(LoanCOBConstant.LOAN_IDS)).get(0));
+            }
+        });
+
+        Then("throw exception LoanCOBPartitioner.partition method", () -> {
+            assertThrows(RuntimeException.class, () -> {
+                resultItem = this.loanCOBPartitioner.partition(2);
+            });
+        });
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java
 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java
new file mode 100644
index 000000000..543f1c03e
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemProcessorStepDefinitions.java
@@ -0,0 +1,82 @@
+/**
+ * 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.cob.loan;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+
+import io.cucumber.java8.En;
+import java.util.TreeMap;
+import org.apache.fineract.cob.COBBusinessStepService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.item.ExecutionContext;
+
+public class LoanItemProcessorStepDefinitions implements En {
+
+    private COBBusinessStepService cobBusinessStepService = 
mock(COBBusinessStepService.class);
+
+    private LoanItemProcessor loanItemProcessor = new 
LoanItemProcessor(cobBusinessStepService);
+
+    private Loan loan = mock(Loan.class);
+
+    private Loan loanItem;
+    private Loan processedLoan = mock(Loan.class);
+
+    private Loan resultItem;
+
+    private TreeMap<Long, String> treeMap = mock(TreeMap.class);
+
+    public LoanItemProcessorStepDefinitions() {
+        Given("/^The LoanItemProcessor.process method with item (.*)$/", 
(String loanItem) -> {
+
+            StepExecution stepExecution = new StepExecution("test", null);
+            ExecutionContext stepExecutionContext = new ExecutionContext();
+            stepExecutionContext.put(LoanCOBConstant.BUSINESS_STEP_MAP, 
treeMap);
+            stepExecution.setExecutionContext(stepExecutionContext);
+            loanItemProcessor.beforeStep(stepExecution);
+
+            if (loanItem.isEmpty()) {
+                this.loanItem = null;
+            } else {
+                this.loanItem = loan;
+            }
+
+            lenient().when(this.cobBusinessStepService.run(treeMap, 
null)).thenThrow(new RuntimeException("fail"));
+            lenient().when(this.cobBusinessStepService.run(treeMap, 
loan)).thenReturn(processedLoan);
+
+        });
+
+        When("LoanItemProcessor.process method executed", () -> {
+            resultItem = this.loanItemProcessor.process(loanItem);
+        });
+
+        Then("LoanItemProcessor.process result should match", () -> {
+            assertEquals(processedLoan, resultItem);
+        });
+
+        Then("throw exception LoanItemProcessor.process method", () -> {
+            assertThrows(RuntimeException.class, () -> {
+                resultItem = this.loanItemProcessor.process(loanItem);
+            });
+        });
+    }
+}
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
new file mode 100644
index 000000000..b519ee32f
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemReaderStepDefinitions.java
@@ -0,0 +1,99 @@
+/**
+ * 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.cob.loan;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+
+import com.google.common.base.Splitter;
+import io.cucumber.java8.En;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.apache.fineract.cob.exceptions.LoanReadException;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.item.ExecutionContext;
+
+public class LoanItemReaderStepDefinitions implements En {
+
+    private LoanRepository loanRepository = mock(LoanRepository.class);
+
+    private LoanItemReader loanItemReader = new LoanItemReader(loanRepository);
+
+    private Loan loan = mock(Loan.class);
+
+    private Loan resultItem;
+
+    public LoanItemReaderStepDefinitions() {
+        Given("/^The LoanItemReader.read method with loanIds (.*), 
lockedAccounts (.*)$/", (String loanIds, String lockedAccounts) -> {
+            JobExecution jobExecution = new JobExecution(1L);
+            ExecutionContext jobExecutionContext = new ExecutionContext();
+            List<Long> splitLockedAccounts;
+            if (lockedAccounts.isEmpty()) {
+                splitLockedAccounts = new ArrayList<>();
+            } else {
+                List<String> splitLockedAccountsStr = 
Splitter.on(',').splitToList(lockedAccounts);
+                splitLockedAccounts = 
splitLockedAccountsStr.stream().map(Long::parseLong).toList();
+            }
+            
jobExecutionContext.put(LoanCOBWorkerConfiguration.ALREADY_LOCKED_LOAN_IDS, new 
ArrayList<>(splitLockedAccounts));
+            jobExecution.setExecutionContext(jobExecutionContext);
+            StepExecution stepExecution = new StepExecution("test", 
jobExecution);
+            ExecutionContext stepExecutionContext = new ExecutionContext();
+            List<Long> splitAccounts;
+            if (loanIds.isEmpty()) {
+                splitAccounts = new ArrayList<>();
+            } else {
+                List<String> splitStr = Splitter.on(',').splitToList(loanIds);
+                splitAccounts = 
splitStr.stream().map(Long::parseLong).toList();
+            }
+            stepExecutionContext.put(LoanCOBConstant.LOAN_IDS, new 
ArrayList<>(splitAccounts));
+            stepExecution.setExecutionContext(stepExecutionContext);
+            loanItemReader.beforeStep(stepExecution);
+
+            
lenient().when(this.loanRepository.findById(0L)).thenReturn(Optional.empty());
+            
lenient().when(this.loanRepository.findById(1L)).thenReturn(Optional.of(loan));
+            lenient().when(this.loanRepository.findById(-1L)).thenThrow(new 
RuntimeException("fail"));
+
+        });
+
+        When("LoanItemReader.read method executed", () -> {
+            resultItem = this.loanItemReader.read();
+        });
+
+        Then("The LoanItemReader.read result should match", () -> {
+            assertEquals(loan, resultItem);
+        });
+
+        Then("The LoanItemReader.read result null", () -> {
+            assertNull(resultItem);
+        });
+
+        Then("throw exception LoanItemReader.read method", () -> {
+            assertThrows(LoanReadException.class, () -> {
+                resultItem = this.loanItemReader.read();
+            });
+        });
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemWriterStepDefinitions.java
 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemWriterStepDefinitions.java
new file mode 100644
index 000000000..9ec051544
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/LoanItemWriterStepDefinitions.java
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.loan;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.cucumber.java8.En;
+import java.util.Collections;
+import java.util.List;
+import org.apache.fineract.cob.domain.LoanAccountLockRepository;
+import org.apache.fineract.cob.domain.LockOwner;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import org.mockito.Mockito;
+
+public class LoanItemWriterStepDefinitions implements En {
+
+    private LoanAccountLockRepository accountLockRepository = 
mock(LoanAccountLockRepository.class);
+    private LoanRepository loanRepository = mock(LoanRepository.class);
+
+    private LoanItemWriter loanItemWriter = new 
LoanItemWriter(accountLockRepository);
+
+    private List<Loan> items;
+
+    public LoanItemWriterStepDefinitions() {
+        Given("/^The LoanItemWriter.write method with action (.*)$/", (String 
action) -> {
+
+            Loan loan = mock(Loan.class);
+            lenient().when(loan.getId()).thenReturn(1L);
+            if (action.equals("error")) {
+                this.items = Collections.emptyList();
+                lenient().doThrow(new 
RuntimeException("fail")).when(this.accountLockRepository)
+                        .deleteByLoanIdInAndLockOwner(Collections.emptyList(), 
LockOwner.LOAN_COB_CHUNK_PROCESSING);
+            } else {
+                this.items = Collections.singletonList(loan);
+                
lenient().doNothing().when(this.accountLockRepository).deleteByLoanIdInAndLockOwner(Mockito.anyList(),
 Mockito.any());
+            }
+            this.loanItemWriter.setRepository(loanRepository);
+        });
+
+        When("LoanItemWriter.write method executed", () -> {
+            this.loanItemWriter.write(items);
+        });
+
+        Then("LoanItemWriter.write result should match", () -> {
+            verify(this.accountLockRepository, 
Mockito.times(1)).deleteByLoanIdInAndLockOwner(Mockito.any(), Mockito.any());
+        });
+
+        Then("throw exception LoanItemWriter.write method", () -> {
+            assertThrows(RuntimeException.class, () -> {
+                this.loanItemWriter.write(items);
+            });
+        });
+    }
+}
diff --git a/fineract-provider/src/test/resources/features/cob/cob.feature 
b/fineract-provider/src/test/resources/features/cob/cob.feature
new file mode 100644
index 000000000..4818ee6d9
--- /dev/null
+++ b/fineract-provider/src/test/resources/features/cob/cob.feature
@@ -0,0 +1,72 @@
+#
+# 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.
+#
+
+Feature: COB
+
+  @cob
+  Scenario Outline: COB Business Step Service - run test
+    Given The COBBusinessStepService.run method with executeMap <executionMap>
+    When COBBusinessStepService.run method executed
+    Then The COBBusinessStepService.run result should match
+
+    Examples:
+      |executionMap|
+      |1,test|
+
+  @cob
+  Scenario Outline: COB Business Step Service - run test failure
+    Given The COBBusinessStepService.run method with executeMap <executionMap>
+    Then throw exception COBBusinessStepService.run method
+
+    Examples:
+      |executionMap|
+      |null|
+      |2,notExist|
+      |3,|
+
+  @cob
+  Scenario Outline: COB Business Step Service - getCOBBusinessStepMap test 
found bean
+    Given The COBBusinessStepService.getCOBBusinessStepMap method with 
businessStepClass <className>, and jobName <jobName>
+    When COBBusinessStepService.getCOBBusinessStepMap method executed
+    Then The COBBusinessStepService.getCOBBusinessStepMap result should match
+
+    Examples:
+      |className|jobName|
+      |LoanCOBBusinessStep|exist|
+
+  @cob
+  Scenario Outline: COB Business Step Service - getCOBBusinessStepMap empty
+    Given The COBBusinessStepService.getCOBBusinessStepMap method with 
businessStepClass <className>, and jobName <jobName>
+    When COBBusinessStepService.getCOBBusinessStepMap method executed
+    Then The COBBusinessStepService.getCOBBusinessStepMap result empty
+
+    Examples:
+      |className|jobName|
+      |LoanCOBBusinessStep|notExist|
+      |empty|exist|
+      |random|exist|
+
+  @cob
+  Scenario Outline: COB Business Step Service - getCOBBusinessStepMap no bean
+    Given The COBBusinessStepService.getCOBBusinessStepMap method with 
businessStepClass <className>, and jobName <jobName>
+    Then The COBBusinessStepService.getCOBBusinessStepMap result exception
+
+    Examples:
+      |className|jobName|
+      |null|notExist|
\ No newline at end of file
diff --git 
a/fineract-provider/src/test/resources/features/cob/common/cob.initialisation.step.feature
 
b/fineract-provider/src/test/resources/features/cob/common/cob.initialisation.step.feature
new file mode 100644
index 000000000..723f7db80
--- /dev/null
+++ 
b/fineract-provider/src/test/resources/features/cob/common/cob.initialisation.step.feature
@@ -0,0 +1,39 @@
+#
+# 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.
+#
+
+Feature: COB Initialisation Step
+
+  @cob
+  Scenario Outline: InitialisationTasklet - run test
+    Given The InitialisationTasklet.execute method with action <action>
+    When InitialisationTasklet.execute method executed
+    Then InitialisationTasklet.execute result should match
+
+    Examples:
+      |action|
+      |good|
+
+  @cob
+  Scenario Outline: InitialisationTasklet - run test: exception
+    Given The InitialisationTasklet.execute method with action <action>
+    Then throw exception InitialisationTasklet.execute method
+
+    Examples:
+      |action|
+      |error|
\ No newline at end of file
diff --git 
a/fineract-provider/src/test/resources/features/cob/listener/cob.loan.item.listener.feature
 
b/fineract-provider/src/test/resources/features/cob/listener/cob.loan.item.listener.feature
new file mode 100644
index 000000000..063ed0541
--- /dev/null
+++ 
b/fineract-provider/src/test/resources/features/cob/listener/cob.loan.item.listener.feature
@@ -0,0 +1,50 @@
+#
+# 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.
+#
+
+Feature: COB Loan item Listener
+
+  @cob
+  Scenario Outline: LoanItemListener.onReadError - run test
+    Given The LoanItemListener.onReadError method <action>
+    When LoanItemListener.onReadError method executed
+    Then LoanItemListener.onReadError result should match
+
+    Examples:
+      |action|
+      |run   |
+
+  @cob
+  Scenario Outline: LoanItemListener.onProcessError - run test
+    Given The LoanItemListener.onProcessError method <action>
+    When LoanItemListener.onProcessError method executed
+    Then LoanItemListener.onProcessError result should match
+
+    Examples:
+      |action|
+      |run   |
+
+  @cob
+  Scenario Outline: LoanItemListener.onWriteError - run test
+    Given The LoanItemListener.onWriteError method <action>
+    When LoanItemListener.onWriteError method executed
+    Then LoanItemListener.onWriteError result should match
+
+    Examples:
+      |action|
+      |run   |
\ No newline at end of file
diff --git 
a/fineract-provider/src/test/resources/features/cob/loan/cob.applylock.step.feature
 
b/fineract-provider/src/test/resources/features/cob/loan/cob.applylock.step.feature
new file mode 100644
index 000000000..e0423cf61
--- /dev/null
+++ 
b/fineract-provider/src/test/resources/features/cob/loan/cob.applylock.step.feature
@@ -0,0 +1,39 @@
+#
+# 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.
+#
+
+Feature: COB Apply Loan Lock Step
+
+  @cob
+  Scenario Outline: ApplyLoanLockTasklet - run test
+    Given The ApplyLoanLockTasklet.execute method with action <action>
+    When ApplyLoanLockTasklet.execute method executed
+    Then ApplyLoanLockTasklet.execute result should match
+
+    Examples:
+      |action|
+      |good|
+
+  @cob
+  Scenario Outline: ApplyLoanLockTasklet - run test: exception
+    Given The ApplyLoanLockTasklet.execute method with action <action>
+    Then throw exception ApplyLoanLockTasklet.execute method
+
+    Examples:
+      |action|
+      |error|
\ No newline at end of file
diff --git 
a/fineract-provider/src/test/resources/features/cob/loan/cob.loan.processor.feature
 
b/fineract-provider/src/test/resources/features/cob/loan/cob.loan.processor.feature
new file mode 100644
index 000000000..cf7d4a7cb
--- /dev/null
+++ 
b/fineract-provider/src/test/resources/features/cob/loan/cob.loan.processor.feature
@@ -0,0 +1,39 @@
+#
+# 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.
+#
+
+Feature: COB Process
+
+  @cob
+  Scenario Outline: LoanItemProcessor - run test
+    Given The LoanItemProcessor.process method with item <item>
+    When LoanItemProcessor.process method executed
+    Then LoanItemProcessor.process result should match
+
+    Examples:
+      |item|
+      |1   |
+
+  @cob
+  Scenario Outline: LoanItemProcessor - run test: exception
+    Given The LoanItemProcessor.process method with item <item>
+    Then throw exception LoanItemProcessor.process method
+
+    Examples:
+      |item|
+      ||
\ No newline at end of file
diff --git 
a/fineract-provider/src/test/resources/features/cob/loan/cob.loan.reader.feature
 
b/fineract-provider/src/test/resources/features/cob/loan/cob.loan.reader.feature
new file mode 100644
index 000000000..01e472c8a
--- /dev/null
+++ 
b/fineract-provider/src/test/resources/features/cob/loan/cob.loan.reader.feature
@@ -0,0 +1,52 @@
+#
+# 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.
+#
+
+Feature: COB Reader
+
+  @cob
+  Scenario Outline: LoanItemReader - run test
+    Given The LoanItemReader.read method with loanIds <loanIds>, 
lockedAccounts <lockedAccounts>
+    When LoanItemReader.read method executed
+    Then The LoanItemReader.read result should match
+
+    Examples:
+      |loanIds|lockedAccounts|
+      |1      |              |
+      |1,2    |3,4           |
+
+  @cob
+  Scenario Outline: LoanItemReader - run test: null
+    Given The LoanItemReader.read method with loanIds <loanIds>, 
lockedAccounts <lockedAccounts>
+    When LoanItemReader.read method executed
+    Then The LoanItemReader.read result null
+
+    Examples:
+      |loanIds|lockedAccounts|
+      |       |              |
+
+  @cob
+  Scenario Outline: LoanItemReader - run test: exception
+    Given The LoanItemReader.read method with loanIds <loanIds>, 
lockedAccounts <lockedAccounts>
+    Then throw exception LoanItemReader.read method
+
+    Examples:
+      |loanIds|lockedAccounts|
+      |-1     |              |
+      |1      | 1            |
+      |0      |              |
\ No newline at end of file
diff --git 
a/fineract-provider/src/test/resources/features/cob/loan/cob.loan.writer.feature
 
b/fineract-provider/src/test/resources/features/cob/loan/cob.loan.writer.feature
new file mode 100644
index 000000000..1ecccaef1
--- /dev/null
+++ 
b/fineract-provider/src/test/resources/features/cob/loan/cob.loan.writer.feature
@@ -0,0 +1,39 @@
+#
+# 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.
+#
+
+Feature: COB Writer
+
+  @cob
+  Scenario Outline: LoanItemWriter - run test
+    Given The LoanItemWriter.write method with action <action>
+    When LoanItemWriter.write method executed
+    Then LoanItemWriter.write result should match
+
+    Examples:
+      |action|
+      |good|
+
+  @cob
+  Scenario Outline: LoanItemWriter - run test: exception
+    Given The LoanItemWriter.write method with action <action>
+    Then throw exception LoanItemWriter.write method
+
+    Examples:
+      |action|
+      |error|
\ No newline at end of file
diff --git 
a/fineract-provider/src/test/resources/features/cob/loan/cob.partitioner.feature
 
b/fineract-provider/src/test/resources/features/cob/loan/cob.partitioner.feature
new file mode 100644
index 000000000..404928ce4
--- /dev/null
+++ 
b/fineract-provider/src/test/resources/features/cob/loan/cob.partitioner.feature
@@ -0,0 +1,40 @@
+#
+# 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.
+#
+
+Feature: COB Loan partitioner Step
+
+  @cob
+  Scenario Outline: LoanCOBPartitioner - run test
+    Given The LoanCOBPartitioner.partition method with action <action>
+    When LoanCOBPartitioner.partition method executed
+    Then LoanCOBPartitioner.partition result should match
+
+    Examples:
+      |action|
+      |empty steps|
+      |good|
+
+  @cob
+  Scenario Outline: LoanCOBPartitioner - run test: exception
+    Given The LoanCOBPartitioner.partition method with action <action>
+    Then throw exception LoanCOBPartitioner.partition method
+
+    Examples:
+      |action|
+      |empty loanIds|
\ No newline at end of file

Reply via email to