You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2023/06/05 09:13:46 UTC

[fineract] branch develop updated: FINERACT-1926: Asset externalisation logic enhancements and bugfixes

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 9698b6d5b FINERACT-1926: Asset externalisation logic enhancements and bugfixes
9698b6d5b is described below

commit 9698b6d5b32f83059469c9d2e66004d7e320cb41
Author: Adam Saghy <ad...@gmail.com>
AuthorDate: Fri Jun 2 16:48:29 2023 +0200

    FINERACT-1926: Asset externalisation logic enhancements and bugfixes
---
 .../service/LoanReadPlatformServiceCommon.java     |   4 -
 .../loan/LoanAccountOwnerTransferBusinessStep.java |  55 ++--
 .../ExternalAssetOwnerTransferRepository.java      |   4 +
 .../ExternalAssetOwnersWriteServiceImpl.java       | 156 ++++++----
 .../LoanAccountOwnerTransferBusinessStepTest.java  | 151 ++++++----
 .../apache/fineract/cob/data/LoanCOBParameter.java |   0
 .../cob/data/LoanIdAndExternalIdAndAccountNo.java  |   0
 .../cob/data/LoanIdAndExternalIdAndStatus.java     |  13 +-
 .../cob/data/LoanIdAndLastClosedBusinessDate.java  |   0
 .../loanaccount/domain/LoanRepository.java         |   8 +-
 .../exception/LoanNotFoundException.java           |   0
 .../fineract/cob/data/LoanIdAndExternalId.java     |  28 --
 .../loanaccount/domain/LoanRepositoryWrapper.java  |   7 -
 .../service/LoanReadPlatformServiceImpl.java       |   8 -
 .../LoanCOBAccountLockCatchupInlineCOBTest.java    |   3 +
 .../common/BusinessStepHelper.java                 |  44 +++
 .../common/ExternalAssetOwnerHelper.java           |  49 +++
 .../ExternalAssetOwnerHelper.java                  |  62 ----
 .../InitiateExternalAssetOwnerTransferTest.java    | 331 ++++++++++++++++-----
 19 files changed, 578 insertions(+), 345 deletions(-)

diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java
index 871be9ab2..985c0b083 100644
--- a/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java
+++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceCommon.java
@@ -18,11 +18,7 @@
  */
 package org.apache.fineract.portfolio.loanaccount.service;
 
-import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData;
-
 public interface LoanReadPlatformServiceCommon {
 
-    LoanIdAndExternalIdData getTransferableLoanIdAndExternalId(Long loanId);
-
     Long getLoanIdByLoanExternalId(String externalId);
 }
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java
index e347aeb3d..5e5ac6273 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java
@@ -53,36 +53,27 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
         List<ExternalAssetOwnerTransfer> transferDataList = externalAssetOwnerTransferRepository.findAll(
                 (root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId),
                         criteriaBuilder.equal(root.get("settlementDate"), settlementDate),
-                        root.get("status").in(
-                                List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE, ExternalTransferStatus.BUYBACK))),
+                        root.get("status").in(List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK))),
                 Sort.by(Sort.Direction.ASC, "id"));
         int size = transferDataList.size();
 
-        if (size > 2) {
-            throw new IllegalStateException(
-                    String.format("Found too many owner transfers(%s) by the settlement date(%s)", size, settlementDate));
-        } else if (size == 2) {
+        if (size == 2) {
             ExternalTransferStatus firstTransferStatus = transferDataList.get(0).getStatus();
             ExternalTransferStatus secondTransferStatus = transferDataList.get(1).getStatus();
 
-            if (!ExternalTransferStatus.BUYBACK.equals(secondTransferStatus)) {
-                throw new IllegalStateException(String.format("Illegal transfer found. Expected %s, found: %s",
-                        ExternalTransferStatus.BUYBACK, secondTransferStatus));
-            }
-
-            switch (firstTransferStatus) {
-                case PENDING -> handleSameDaySaleAndBuyback(settlementDate, transferDataList);
-                case ACTIVE -> handleBuyback(loan, settlementDate, transferDataList);
-                default -> throw new IllegalStateException(String.format("Illegal transfer found. Expected %s or %s, found: %s",
-                        ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE, firstTransferStatus));
+            if (!ExternalTransferStatus.PENDING.equals(firstTransferStatus)
+                    || !ExternalTransferStatus.BUYBACK.equals(secondTransferStatus)) {
+                throw new IllegalStateException(String.format("Illegal transfer found. Expected %s and %s, found: %s and %s",
+                        ExternalTransferStatus.PENDING, ExternalTransferStatus.BUYBACK, firstTransferStatus, secondTransferStatus));
             }
+            handleSameDaySaleAndBuyback(settlementDate, transferDataList);
         } else if (size == 1) {
-            ExternalAssetOwnerTransfer externalAssetOwnerTransfer = transferDataList.get(0);
-            if (!ExternalTransferStatus.PENDING.equals(externalAssetOwnerTransfer.getStatus())) {
-                throw new IllegalStateException(String.format("Illegal transfer found. Expected %s, found: %s",
-                        ExternalTransferStatus.PENDING, externalAssetOwnerTransfer.getStatus()));
+            ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
+            if (ExternalTransferStatus.PENDING.equals(transfer.getStatus())) {
+                handleSale(loan, settlementDate, transfer);
+            } else if (ExternalTransferStatus.BUYBACK.equals(transfer.getStatus())) {
+                handleBuyback(loan, settlementDate, transfer);
             }
-            handleSale(loan, settlementDate, externalAssetOwnerTransfer);
         }
 
         log.debug("end processing loan ownership transfer business step for loan Id [{}]", loan.getId());
@@ -95,20 +86,25 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
     }
 
     private void handleBuyback(final Loan loan, final LocalDate settlementDate,
-            final List<ExternalAssetOwnerTransfer> externalAssetOwnerTransferList) {
-        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, externalAssetOwnerTransferList);
+            final ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer) {
+        ExternalAssetOwnerTransfer activeExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository
+                .findOne((root, query, criteriaBuilder) -> criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
+                        criteriaBuilder.equal(root.get("ownerId"), buybackExternalAssetOwnerTransfer.getOwnerId()),
+                        criteriaBuilder.equal(root.get("status"), ExternalTransferStatus.ACTIVE),
+                        criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)))
+                .orElseThrow();
+        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = buybackAsset(loan, settlementDate, buybackExternalAssetOwnerTransfer,
+                activeExternalAssetOwnerTransfer);
         // TODO: trigger asset loan transfer executed event
     }
 
     private ExternalAssetOwnerTransfer buybackAsset(final Loan loan, final LocalDate settlementDate,
-            List<ExternalAssetOwnerTransfer> externalAssetOwnerTransferList) {
-        ExternalAssetOwnerTransfer saleExternalAssetOwnerTransfer = externalAssetOwnerTransferList.get(0);
-        ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer = externalAssetOwnerTransferList.get(1);
-        saleExternalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
+            ExternalAssetOwnerTransfer buybackExternalAssetOwnerTransfer, ExternalAssetOwnerTransfer activeExternalAssetOwnerTransfer) {
+        activeExternalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
         buybackExternalAssetOwnerTransfer.setEffectiveDateTo(buybackExternalAssetOwnerTransfer.getEffectiveDateFrom());
-        externalAssetOwnerTransferRepository.save(saleExternalAssetOwnerTransfer);
+        externalAssetOwnerTransferRepository.save(activeExternalAssetOwnerTransfer);
         buybackExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(buybackExternalAssetOwnerTransfer);
-        externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), buybackExternalAssetOwnerTransfer);
+        externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(), activeExternalAssetOwnerTransfer);
         // TODO: create asset ownership accounting entries
         // TODO: create asset ownership transaction entries
         return buybackExternalAssetOwnerTransfer;
@@ -161,6 +157,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
             final ExternalTransferSubStatus subStatus, final LocalDate effectiveDateFrom, final LocalDate effectiveDateTo) {
         ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
         newExternalAssetOwnerTransfer.setOwner(externalAssetOwnerTransfer.getOwner());
+        newExternalAssetOwnerTransfer.setOwnerId(externalAssetOwnerTransfer.getOwnerId());
         newExternalAssetOwnerTransfer.setExternalId(externalAssetOwnerTransfer.getExternalId());
         newExternalAssetOwnerTransfer.setStatus(status);
         newExternalAssetOwnerTransfer.setSubStatus(subStatus);
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java
index 270525ab9..b4ad2e54c 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferRepository.java
@@ -18,6 +18,8 @@
  */
 package org.apache.fineract.investor.domain;
 
+import java.time.LocalDate;
+import java.util.List;
 import java.util.Optional;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.springframework.data.domain.Page;
@@ -39,4 +41,6 @@ public interface ExternalAssetOwnerTransferRepository
     @Query("select e from ExternalAssetOwnerTransfer e where e.loanId = :loanId and e.id = (select max(ex.id) from ExternalAssetOwnerTransfer ex where ex.loanId = :loanId)")
     Optional<ExternalAssetOwnerTransfer> findLatestByLoanId(@Param("loanId") Long loanId);
 
+    @Query("SELECT t FROM ExternalAssetOwnerTransfer t WHERE t.loanId = :loanId AND t.effectiveDateTo > :effectiveDate")
+    List<ExternalAssetOwnerTransfer> findEffectiveTransfers(@Param("loanId") Long loanId, @Param("effectiveDate") LocalDate effectiveDate);
 }
diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java
index 1dc7c4196..25df924a7 100644
--- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java
+++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceImpl.java
@@ -33,16 +33,17 @@ import java.util.Optional;
 import java.util.Set;
 import lombok.RequiredArgsConstructor;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus;
 import org.apache.fineract.infrastructure.core.api.JsonCommand;
 import org.apache.fineract.infrastructure.core.data.ApiParameterError;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
 import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
 import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
-import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
 import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
 import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
 import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.apache.fineract.investor.data.ExternalTransferRequestParameters;
@@ -53,7 +54,9 @@ import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
 import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
 import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
 import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException;
-import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceCommon;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -61,36 +64,96 @@ import org.springframework.transaction.annotation.Transactional;
 @RequiredArgsConstructor
 public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersWriteService {
 
+    private static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
+    private static final List<LoanStatus> NON_CLOSED_LOAN_STATUSES = List.of(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, LoanStatus.APPROVED,
+            LoanStatus.ACTIVE, LoanStatus.TRANSFER_IN_PROGRESS, LoanStatus.TRANSFER_ON_HOLD);
+    private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING,
+            ExternalTransferStatus.ACTIVE);
     private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
     private final ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
     private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
     private final FromJsonHelper fromApiJsonHelper;
-    private final LoanReadPlatformServiceCommon loanReadPlatformService;
+    private final LoanRepository loanRepository;
 
     @Override
     @Transactional
     public CommandProcessingResult saleLoanByLoanId(JsonCommand command) {
+        final JsonElement json = fromApiJsonHelper.parse(command.json());
         Long loanId = command.getLoanId();
-        LoanIdAndExternalIdData loanIdAndExternalId = loanReadPlatformService.getTransferableLoanIdAndExternalId(loanId);
-        validateLoanStatus(loanIdAndExternalId);
-        ExternalAssetOwnerTransfer externalAssetOwnerTransfer = parseJson(loanId, command.json(), loanIdAndExternalId.getLoanExternalId(),
-                ExternalTransferStatus.PENDING);
+        LocalDate settlementDate = getSettlementDateFromJson(json);
+        LoanIdAndExternalIdAndStatus loanIdAndExternalIdAndStatus = fetchLoanDetails(loanId);
+        validateLoanStatus(loanIdAndExternalIdAndStatus);
+        ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createSaleTransfer(loanId, command.json(),
+                loanIdAndExternalIdAndStatus.getExternalId());
         validateSale(externalAssetOwnerTransfer);
-        ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
-        return buildResponseData(savedExternalAssetOwnerTransfer);
+        externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
+        return buildResponseData(externalAssetOwnerTransfer);
     }
 
     @Override
     @Transactional
     public CommandProcessingResult buybackLoanByLoanId(JsonCommand command) {
+        final JsonElement json = fromApiJsonHelper.parse(command.json());
         Long loanId = command.getLoanId();
-        LoanIdAndExternalIdData loanIdAndExternalId = loanReadPlatformService.getTransferableLoanIdAndExternalId(loanId);
-        validateLoanStatus(loanIdAndExternalId);
-        ExternalAssetOwnerTransfer externalAssetOwnerTransfer = parseJson(loanId, command.json(), loanIdAndExternalId.getLoanExternalId(),
-                ExternalTransferStatus.BUYBACK);
-        validateBuyBack(externalAssetOwnerTransfer);
-        ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer = externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
-        return buildResponseData(savedExternalAssetOwnerTransfer);
+        LocalDate settlementDate = getSettlementDateFromJson(json);
+        ExternalId externalId = getTransferExternalIdFromJson(json);
+        LoanIdAndExternalIdAndStatus loanIdAndExternalIdAndStatus = fetchLoanDetails(loanId);
+        validateLoanStatus(loanIdAndExternalIdAndStatus);
+        validateSettlementDate(settlementDate);
+        ExternalAssetOwnerTransfer effectiveTransfer = fetchAndValidateEffectiveTransferForBuyback(loanId, settlementDate);
+        ExternalAssetOwnerTransfer externalAssetOwnerTransfer = createBuybackTransfer(effectiveTransfer, settlementDate, externalId);
+        externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
+        return buildResponseData(externalAssetOwnerTransfer);
+    }
+
+    private void validateEffectiveTransferForSale(final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
+        List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository
+                .findEffectiveTransfers(externalAssetOwnerTransfer.getLoanId(), externalAssetOwnerTransfer.getSettlementDate());
+
+        if (effectiveTransfers.size() > 0) {
+            throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
+        }
+    }
+
+    private ExternalAssetOwnerTransfer fetchAndValidateEffectiveTransferForBuyback(final Long loanId, final LocalDate settlementDate) {
+        List<ExternalAssetOwnerTransfer> effectiveTransfers = externalAssetOwnerTransferRepository.findEffectiveTransfers(loanId,
+                settlementDate);
+
+        if (effectiveTransfers.size() == 0) {
+            throw new ExternalAssetOwnerInitiateTransferException(
+                    "This loan cannot be bought back, it is not owned by an external asset owner");
+        } else if (effectiveTransfers.size() == 2) {
+            throw new ExternalAssetOwnerInitiateTransferException(
+                    "This loan cannot be bought back, external asset owner buyback transfer is already in progress");
+        } else if (!BUYBACK_READY_STATUSES.contains(effectiveTransfers.get(0).getStatus())) {
+            throw new ExternalAssetOwnerInitiateTransferException(
+                    String.format("This loan cannot be bought back, effective transfer is not in right state: %s",
+                            effectiveTransfers.get(0).getStatus()));
+        } else if (settlementDate.isBefore(effectiveTransfers.get(0).getSettlementDate())) {
+            throw new ExternalAssetOwnerInitiateTransferException(
+                    String.format("This loan cannot be bought back, settlement date is earlier than effective transfer settlement date: %s",
+                            effectiveTransfers.get(0).getSettlementDate()));
+        }
+
+        return effectiveTransfers.get(0);
+    }
+
+    private ExternalAssetOwnerTransfer createBuybackTransfer(ExternalAssetOwnerTransfer effectiveTransfer, LocalDate settlementDate,
+            ExternalId externalId) {
+        LocalDate effectiveDateFrom = DateUtils.getBusinessLocalDate();
+
+        ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
+        externalAssetOwnerTransfer.setExternalId(externalId);
+        externalAssetOwnerTransfer.setOwnerId(effectiveTransfer.getOwnerId());
+        externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.BUYBACK);
+        externalAssetOwnerTransfer.setLoanId(effectiveTransfer.getLoanId());
+        externalAssetOwnerTransfer.setExternalLoanId(effectiveTransfer.getExternalLoanId());
+        externalAssetOwnerTransfer.setOwner(effectiveTransfer.getOwner());
+        externalAssetOwnerTransfer.setSettlementDate(settlementDate);
+        externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
+        externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31);
+        externalAssetOwnerTransfer.setPurchasePriceRatio(effectiveTransfer.getPurchasePriceRatio());
+        return externalAssetOwnerTransfer;
     }
 
     private CommandProcessingResult buildResponseData(ExternalAssetOwnerTransfer savedExternalAssetOwnerTransfer) {
@@ -98,8 +161,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
         changes.put(ExternalTransferRequestParameters.SETTLEMENT_DATE, savedExternalAssetOwnerTransfer.getSettlementDate());
         changes.put(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID,
                 savedExternalAssetOwnerTransfer.getOwner().getExternalId().getValue());
-        changes.put(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID,
-                savedExternalAssetOwnerTransfer.getOwner().getExternalId().getValue());
+        changes.put(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, savedExternalAssetOwnerTransfer.getExternalId().getValue());
         changes.put(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, savedExternalAssetOwnerTransfer.getPurchasePriceRatio());
         return new CommandProcessingResultBuilder().withEntityId(savedExternalAssetOwnerTransfer.getId())
                 .withEntityExternalId(savedExternalAssetOwnerTransfer.getExternalId())
@@ -112,11 +174,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
     private void validateSale(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
         validateSettlementDate(externalAssetOwnerTransfer);
         validateTransferStatusForSale(externalAssetOwnerTransfer);
-    }
-
-    private void validateBuyBack(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
-        validateSettlementDate(externalAssetOwnerTransfer);
-        validateTransferStatusForBuyBack(externalAssetOwnerTransfer);
+        validateEffectiveTransferForSale(externalAssetOwnerTransfer);
     }
 
     private void validateSettlementDate(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
@@ -125,9 +183,14 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
         }
     }
 
-    private void validateLoanStatus(LoanIdAndExternalIdData loanIdAndExternalIdAndExternalId) {
-        if (Objects.isNull(loanIdAndExternalIdAndExternalId.getLoanId())
-                && Objects.isNull(loanIdAndExternalIdAndExternalId.getLoanExternalId())) {
+    private void validateSettlementDate(LocalDate settlementDate) {
+        if (settlementDate.isBefore(ThreadLocalContextUtil.getBusinessDate())) {
+            throw new ExternalAssetOwnerInitiateTransferException("Settlement date cannot be in the past");
+        }
+    }
+
+    private void validateLoanStatus(LoanIdAndExternalIdAndStatus entity) {
+        if (!NON_CLOSED_LOAN_STATUSES.contains(LoanStatus.fromInt(entity.getLoanStatus()))) {
             throw new ExternalAssetOwnerInitiateTransferException("Loan is not in active status");
         }
     }
@@ -140,52 +203,35 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
             ExternalTransferStatus latestTransferStatus = latestTransfer.getStatus();
             if (latestTransferStatus.equals(ExternalTransferStatus.PENDING)) {
                 throw new ExternalAssetOwnerInitiateTransferException(
-                        "External asset owner transfer is already in PENDING state for this loan.");
+                        "External asset owner transfer is already in PENDING state for this loan");
             } else if (latestTransferStatus.equals(ExternalTransferStatus.ACTIVE)) {
                 throw new ExternalAssetOwnerInitiateTransferException(
-                        "This loan cannot be sold, because it is owned by an external asset owner.");
+                        "This loan cannot be sold, because it is owned by an external asset owner");
             }
         }
     }
 
-    private void validateTransferStatusForBuyBack(ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
-        Optional<ExternalAssetOwnerTransfer> latestTransferOptional = externalAssetOwnerTransferRepository
-                .findLatestByLoanId(externalAssetOwnerTransfer.getLoanId());
-        if (latestTransferOptional.isEmpty()) {
-            throw new ExternalAssetOwnerInitiateTransferException(
-                    "This loan cannot be bought back, because it is not owned by an external asset owner");
-        } else {
-            ExternalAssetOwnerTransfer latestTransfer = latestTransferOptional.get();
-            ExternalTransferStatus latestTransferStatus = latestTransfer.getStatus();
-            if (latestTransferStatus.equals(ExternalTransferStatus.BUYBACK)) {
-                throw new ExternalAssetOwnerInitiateTransferException(
-                        "External asset owner transfer is already in BUYBACK state for this loan.");
-            }
-        }
-    }
-
-    private ExternalAssetOwnerTransfer parseJson(Long loanId, String apiRequestBodyAsJson, ExternalId externalLoanId,
-            ExternalTransferStatus status) {
+    private ExternalAssetOwnerTransfer createSaleTransfer(Long loanId, String apiRequestBodyAsJson, ExternalId externalLoanId) {
         ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
-
-        validateRequestBody(apiRequestBodyAsJson);
+        LocalDate effectiveFrom = ThreadLocalContextUtil.getBusinessDate();
+        validateSaleRequestBody(apiRequestBodyAsJson);
         final JsonElement json = fromApiJsonHelper.parse(apiRequestBodyAsJson);
 
         ExternalAssetOwner owner = getOwner(json);
         externalAssetOwnerTransfer.setOwnerId(owner.getId());
         externalAssetOwnerTransfer.setOwner(owner);
         externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json));
-        externalAssetOwnerTransfer.setStatus(status);
+        externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING);
         externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json));
         externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json));
-        externalAssetOwnerTransfer.setEffectiveDateFrom(ThreadLocalContextUtil.getBusinessDate());
-        externalAssetOwnerTransfer.setEffectiveDateTo(LocalDate.of(9999, 12, 31));
+        externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveFrom);
+        externalAssetOwnerTransfer.setEffectiveDateTo(FUTURE_DATE_9999_12_31);
         externalAssetOwnerTransfer.setLoanId(loanId);
         externalAssetOwnerTransfer.setExternalLoanId(externalLoanId);
         return externalAssetOwnerTransfer;
     }
 
-    private void validateRequestBody(String apiRequestBodyAsJson) {
+    private void validateSaleRequestBody(String apiRequestBodyAsJson) {
         final Set<String> requestParameters = new HashSet<>(
                 Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, ExternalTransferRequestParameters.OWNER_EXTERNAL_ID,
                         ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO,
@@ -248,4 +294,10 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
         externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId));
         return externalAssetOwnerRepository.saveAndFlush(externalAssetOwner);
     }
+
+    private LoanIdAndExternalIdAndStatus fetchLoanDetails(Long loanId) {
+        Optional<LoanIdAndExternalIdAndStatus> loanIdAndExternalIdAndStatusResult = loanRepository
+                .findLoanIdAndExternalIdAndStatusByLoanId(loanId);
+        return loanIdAndExternalIdAndStatusResult.orElseThrow(() -> new LoanNotFoundException(loanId));
+    }
 }
diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java
index e4f8e7d9c..9492e630b 100644
--- a/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java
+++ b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java
@@ -33,6 +33,7 @@ import java.time.ZoneId;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
 import org.apache.fineract.infrastructure.core.domain.ActionContext;
 import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
@@ -80,7 +81,6 @@ public class LoanAccountOwnerTransferBusinessStepTest {
         // given
         final Loan loanForProcessing = Mockito.mock(Loan.class);
         Long loanId = 1L;
-        LocalDate settlementDate = actualDate;
         when(loanForProcessing.getId()).thenReturn(loanId);
         // when
         final Loan processedLoan = underTest.execute(loanForProcessing);
@@ -90,33 +90,13 @@ public class LoanAccountOwnerTransferBusinessStepTest {
     }
 
     @Test
-    public void givenLoanTooManyTransfer() {
-        // given
-        final Loan loanForProcessing = Mockito.mock(Loan.class);
-        Long loanId = 1L;
-        LocalDate settlementDate = actualDate;
-        when(loanForProcessing.getId()).thenReturn(loanId);
-        ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
-        ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
-        ExternalAssetOwnerTransfer thirdResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
-        List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, secondResponseItem, thirdResponseItem);
-        when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
-                .thenReturn(response);
-        // when
-        IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing));
-        // then
-        assertEquals("Found too many owner transfers(3) by the settlement date(" + actualDate + ")", exception.getMessage());
-        verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
-
-    }
-
-    @Test
-    public void givenLoanTwoTransferButInvalidSecondTransfer() {
+    public void givenLoanTwoTransferButInvalidTransfers() {
         // given
         final Loan loanForProcessing = Mockito.mock(Loan.class);
         when(loanForProcessing.getId()).thenReturn(1L);
         ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
         ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
+        when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
         when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE);
         List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, secondResponseItem);
         when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
@@ -124,26 +104,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
         // when
         IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing));
         // then
-        assertEquals("Illegal transfer found. Expected BUYBACK, found: ACTIVE", exception.getMessage());
-        verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
-    }
-
-    @Test
-    public void givenLoanTwoTransferButInvalidFirstTransfer() {
-        // given
-        final Loan loanForProcessing = Mockito.mock(Loan.class);
-        when(loanForProcessing.getId()).thenReturn(1L);
-        ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
-        ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
-        when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
-        when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
-        List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, secondResponseItem);
-        when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
-                .thenReturn(response);
-        // when
-        IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing));
-        // then
-        assertEquals("Illegal transfer found. Expected PENDING or ACTIVE, found: BUYBACK", exception.getMessage());
+        assertEquals("Illegal transfer found. Expected PENDING and BUYBACK, found: PENDING and ACTIVE", exception.getMessage());
         verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
     }
 
@@ -208,11 +169,12 @@ public class LoanAccountOwnerTransferBusinessStepTest {
         when(loanForProcessing.getId()).thenReturn(1L);
         ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
         ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
-        when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE);
-        when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
-        List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, secondResponseItem);
+        when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
+        when(firstResponseItem.getEffectiveDateFrom()).thenReturn(actualDate);
+        List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
         when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
                 .thenReturn(response);
+        when(externalAssetOwnerTransferRepository.findOne(any(Specification.class))).thenReturn(Optional.of(secondResponseItem));
         ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
                 .forClass(ExternalAssetOwnerTransfer.class);
         when(externalAssetOwnerTransferRepository.save(firstResponseItem)).thenReturn(firstResponseItem);
@@ -221,33 +183,61 @@ public class LoanAccountOwnerTransferBusinessStepTest {
         final Loan processedLoan = underTest.execute(loanForProcessing);
         // then
         verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
-        verify(firstResponseItem).setEffectiveDateTo(actualDate);
+        verify(firstResponseItem).setEffectiveDateTo(firstResponseItem.getEffectiveDateFrom());
         verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
-        verify(secondResponseItem).setEffectiveDateTo(secondResponseItem.getEffectiveDateFrom());
+        verify(secondResponseItem).setEffectiveDateTo(actualDate);
         verify(externalAssetOwnerTransferLoanMappingRepository, times(1)).deleteByLoanIdAndOwnerTransfer(1L, secondResponseItem);
 
         assertEquals(processedLoan, loanForProcessing);
     }
 
     @Test
-    public void givenLoanOneTransferButInvalidTransfer() {
+    public void givenLoanSale() {
         // given
         final Loan loanForProcessing = Mockito.mock(Loan.class);
         when(loanForProcessing.getId()).thenReturn(1L);
         ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
-        when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE);
+        when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
         List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
         when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
                 .thenReturn(response);
+        ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
+                .forClass(ExternalAssetOwnerTransfer.class);
+        ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping> externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor
+                .forClass(ExternalAssetOwnerTransferLoanMapping.class);
+        ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
+        when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+        when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE);
         // when
-        IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing));
+        final Loan processedLoan = underTest.execute(loanForProcessing);
         // then
-        assertEquals("Illegal transfer found. Expected PENDING, found: ACTIVE", exception.getMessage());
         verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
+        verify(firstResponseItem).setEffectiveDateTo(actualDate);
+        verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
+
+        assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
+                externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
+        assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
+                externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
+        assertEquals(ExternalTransferStatus.ACTIVE, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
+        assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
+        assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
+                externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
+        assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
+                externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
+        assertEquals(actualDate.plusDays(1), externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
+        assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
+        verify(externalAssetOwnerTransferLoanMappingRepository, times(1))
+                .save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
+        assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
+        assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
+        assertEquals(processedLoan, loanForProcessing);
     }
 
     @Test
-    public void givenLoanSale() {
+    public void givenLoanSaleButBalanceIsZero() {
         // given
         final Loan loanForProcessing = Mockito.mock(Loan.class);
         when(loanForProcessing.getId()).thenReturn(1L);
@@ -258,13 +248,12 @@ public class LoanAccountOwnerTransferBusinessStepTest {
                 .thenReturn(response);
         ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
                 .forClass(ExternalAssetOwnerTransfer.class);
-        ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping> externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor
-                .forClass(ExternalAssetOwnerTransferLoanMapping.class);
         ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
         when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
         LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
         when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
-        when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE);
+        when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
+        when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ZERO);
         // when
         final Loan processedLoan = underTest.execute(loanForProcessing);
         // then
@@ -276,18 +265,54 @@ public class LoanAccountOwnerTransferBusinessStepTest {
                 externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
         assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
                 externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
-        assertEquals(ExternalTransferStatus.ACTIVE, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
+        assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
         assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
         assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
                 externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
         assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
                 externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
-        assertEquals(actualDate.plusDays(1), externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
-        assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
-        verify(externalAssetOwnerTransferLoanMappingRepository, times(1))
-                .save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
-        assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
-        assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
+        assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
+        assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
+        assertEquals(processedLoan, loanForProcessing);
+    }
+
+    @Test
+    public void givenLoanSaleButBalanceIsNegative() {
+        // given
+        final Loan loanForProcessing = Mockito.mock(Loan.class);
+        when(loanForProcessing.getId()).thenReturn(1L);
+        ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
+        when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
+        List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
+        when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
+                .thenReturn(response);
+        ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
+                .forClass(ExternalAssetOwnerTransfer.class);
+        ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
+        when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+        when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE.negate());
+        when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE.negate());
+        // when
+        final Loan processedLoan = underTest.execute(loanForProcessing);
+        // then
+        verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
+        verify(firstResponseItem).setEffectiveDateTo(actualDate);
+        verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
+
+        assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
+                externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
+        assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
+                externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
+        assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
+        assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
+        assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
+                externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
+        assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
+                externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
+        assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
+        assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
         assertEquals(processedLoan, loanForProcessing);
     }
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java
rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanCOBParameter.java
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndAccountNo.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndAccountNo.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndAccountNo.java
rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndAccountNo.java
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java
similarity index 83%
rename from fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java
rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java
index f09191a3a..92c272939 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/LoanIdAndExternalIdData.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalIdAndStatus.java
@@ -16,16 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.core.data;
+package org.apache.fineract.cob.data;
 
 import lombok.AllArgsConstructor;
-import lombok.Data;
+import lombok.Getter;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
 
-@Data
 @AllArgsConstructor
-public class LoanIdAndExternalIdData {
+@Getter
+public class LoanIdAndExternalIdAndStatus {
 
-    private Long loanId;
-    private ExternalId loanExternalId;
+    Long id;
+    ExternalId externalId;
+    Integer loanStatus;
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java b/fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java
rename to fineract-loan/src/main/java/org/apache/fineract/cob/data/LoanIdAndLastClosedBusinessDate.java
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java
similarity index 97%
rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java
rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java
index 3f7952d11..70081a7b7 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepository.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Optional;
 import org.apache.fineract.cob.data.LoanCOBParameter;
 import org.apache.fineract.cob.data.LoanIdAndExternalIdAndAccountNo;
+import org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus;
 import org.apache.fineract.cob.data.LoanIdAndLastClosedBusinessDate;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.springframework.data.jpa.repository.JpaRepository;
@@ -86,8 +87,7 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
 
     String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where loan.accountNumber = :accountNumber";
 
-    String GET_NON_CLOSED_LOAN_BY_LOAN_ID = "select loan from Loan loan where loan.id = :loanId and loan.loanStatus in (100,200,300,"
-            + "303,304)";
+    String FIND_LOAN_ID_AND_EXTERNAL_ID_AND_STATUS = "select new org.apache.fineract.cob.data.LoanIdAndExternalIdAndStatus(loan.id, loan.externalId, loan.loanStatus) from Loan loan where loan.id = :loanId";
 
     String EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID = "select case when (count (loan) > 0) then 'true' else 'false' end from Loan loan where loan.externalId = :externalLoanId and loan.loanStatus in (100,200,300,303,304)";
 
@@ -197,8 +197,8 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
     @Query(FIND_BY_ACCOUNT_NUMBER)
     Loan findLoanAccountByAccountNumber(@Param("accountNumber") String accountNumber);
 
-    @Query(GET_NON_CLOSED_LOAN_BY_LOAN_ID)
-    Optional<Loan> getNonClosedLoanIdAndExternalIdByLoanId(@Param("loanId") Long loanId);
+    @Query(FIND_LOAN_ID_AND_EXTERNAL_ID_AND_STATUS)
+    Optional<LoanIdAndExternalIdAndStatus> findLoanIdAndExternalIdAndStatusByLoanId(@Param("loanId") Long loanId);
 
     @Query(EXISTS_NON_CLOSED_BY_EXTERNAL_LOAN_ID)
     boolean existsNonClosedLoanByExternalLoanId(@Param("externalLoanId") ExternalId externalLoanId);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java
similarity index 100%
rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java
rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/exception/LoanNotFoundException.java
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java b/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java
deleted file mode 100644
index 40a0584f2..000000000
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/data/LoanIdAndExternalId.java
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.cob.data;
-
-import org.apache.fineract.infrastructure.core.domain.ExternalId;
-
-public interface LoanIdAndExternalId {
-
-    Long getId();
-
-    ExternalId getExternalId();
-}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java
index f072a5073..959d71eda 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepositoryWrapper.java
@@ -24,7 +24,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
-import java.util.Optional;
 import lombok.RequiredArgsConstructor;
 import org.apache.fineract.infrastructure.core.config.FineractProperties;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
@@ -264,10 +263,4 @@ public class LoanRepositoryWrapper {
         return repository.findLoanIdByStatusId(statusId);
     }
 
-    public Optional<Loan> getNonClosedLoanIdAndExternalIdByLoanId(Long loanId) {
-        if (repository.findById(loanId).isEmpty()) {
-            throw new LoanNotFoundException(loanId);
-        }
-        return repository.getNonClosedLoanIdAndExternalIdByLoanId(loanId);
-    }
 }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
index d9f15f469..710fa04fd 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java
@@ -42,7 +42,6 @@ import org.apache.fineract.infrastructure.codes.data.CodeValueData;
 import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
 import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
 import org.apache.fineract.infrastructure.core.data.EnumOptionData;
-import org.apache.fineract.infrastructure.core.data.LoanIdAndExternalIdData;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -593,13 +592,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
         }
     }
 
-    @Override
-    public LoanIdAndExternalIdData getTransferableLoanIdAndExternalId(Long loanId) {
-        Optional<Loan> loan = loanRepositoryWrapper.getNonClosedLoanIdAndExternalIdByLoanId(loanId);
-        return loan.map(value -> new LoanIdAndExternalIdData(value.getId(), value.getExternalId()))
-                .orElseGet(() -> new LoanIdAndExternalIdData(null, null));
-    }
-
     @Override
     public Long getLoanIdByLoanExternalId(String externalId) {
         ExternalId loanExternalId = ExternalIdFactory.produce(externalId);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBAccountLockCatchupInlineCOBTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBAccountLockCatchupInlineCOBTest.java
index 1e11607d5..2ef563e77 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBAccountLockCatchupInlineCOBTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBAccountLockCatchupInlineCOBTest.java
@@ -43,12 +43,15 @@ import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuil
 import org.apache.fineract.integrationtests.common.loans.LoanCOBCatchUpHelper;
 import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
 import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
+import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
 import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
 import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
 
+@ExtendWith(LoanTestLifecycleExtension.class)
 public class LoanCOBAccountLockCatchupInlineCOBTest {
 
     private ResponseSpecification responseSpec;
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java
new file mode 100644
index 000000000..0f30e6fe1
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/BusinessStepHelper.java
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests.common;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.fineract.client.models.BusinessStep;
+import org.apache.fineract.client.models.UpdateBusinessStepConfigRequest;
+import org.apache.fineract.integrationtests.client.IntegrationTest;
+
+public class BusinessStepHelper extends IntegrationTest {
+
+    public BusinessStepHelper() {}
+
+    public void updateSteps(String jobName, String... steps) {
+        long order = 0;
+        List<BusinessStep> stepList = new ArrayList<>();
+        for (String step : steps) {
+            order++;
+            BusinessStep businessStep = new BusinessStep();
+            businessStep.stepName(step);
+            businessStep.setOrder(order);
+            stepList.add(businessStep);
+        }
+        ok(fineract().businessStepConfiguration.updateJobBusinessStepConfig(jobName,
+                new UpdateBusinessStepConfigRequest().businessSteps(stepList)));
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalAssetOwnerHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalAssetOwnerHelper.java
new file mode 100644
index 000000000..d114e124f
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalAssetOwnerHelper.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests.common;
+
+import org.apache.fineract.client.models.PageExternalTransferData;
+import org.apache.fineract.client.models.PostInitiateTransferRequest;
+import org.apache.fineract.client.models.PostInitiateTransferResponse;
+import org.apache.fineract.integrationtests.client.IntegrationTest;
+
+public class ExternalAssetOwnerHelper extends IntegrationTest {
+
+    public ExternalAssetOwnerHelper() {}
+
+    public PostInitiateTransferResponse initiateTransferByLoanId(Long loanId, String command, PostInitiateTransferRequest request) {
+        return ok(fineract().externalAssetOwners.transferRequestWithLoanId(loanId, request, command));
+    }
+
+    public PageExternalTransferData retrieveTransferByTransferExternalId(String transferExternalId) {
+        return ok(fineract().externalAssetOwners.getTransfer1(transferExternalId, null, null, 0, 100));
+    }
+
+    public PageExternalTransferData retrieveTransferByLoanExternalId(String loanExternalId) {
+        return ok(fineract().externalAssetOwners.getTransfer1(null, null, loanExternalId, 0, 100));
+    }
+
+    public PageExternalTransferData retrieveTransferByLoanId(Long loanId) {
+        return ok(fineract().externalAssetOwners.getTransfer1(null, loanId, null, 0, 100));
+    }
+
+    public PageExternalTransferData retrieveTransferByLoanId(Long loanId, int offset, int limit) {
+        return ok(fineract().externalAssetOwners.getTransfer1(null, loanId, null, offset, limit));
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java
deleted file mode 100644
index 2270f8f1c..000000000
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerHelper.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.fineract.integrationtests.investor.externalassetowner;
-
-import io.restassured.specification.RequestSpecification;
-import io.restassured.specification.ResponseSpecification;
-import org.apache.fineract.client.models.PageExternalTransferData;
-import org.apache.fineract.integrationtests.client.IntegrationTest;
-import org.apache.fineract.integrationtests.common.Utils;
-
-public class ExternalAssetOwnerHelper extends IntegrationTest {
-
-    private final RequestSpecification requestSpec;
-    private final ResponseSpecification responseSpec;
-
-    public ExternalAssetOwnerHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
-        this.requestSpec = requestSpec;
-        this.responseSpec = responseSpec;
-    }
-
-    public String initiateTransferByLoanId(Long loanId, String command, String json) {
-        final String INITIATE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers/loans/" + loanId + "?"
-                + Utils.TENANT_IDENTIFIER + "&command=" + command;
-        return Utils.performServerPost(requestSpec, responseSpec, INITIATE_TRANSFER_URL, json);
-    }
-
-    public String initiateTransferByLoanExternalId(String loanExternalId, String command, String json) {
-        final String INITIATE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers/loans/external-id/" + loanExternalId
-                + "?" + Utils.TENANT_IDENTIFIER + "&command=" + command;
-        return Utils.performServerPost(requestSpec, responseSpec, INITIATE_TRANSFER_URL, json);
-    }
-
-    public String retrieveTransferByTransferExternalId(String transferExternalId) {
-        final String RETRIEVE_TRANSFER_URL = "/fineract-provider/api/v1/external-asset-owners/transfers?" + Utils.TENANT_IDENTIFIER
-                + "&transferExternalId=" + transferExternalId;
-        return Utils.performServerGet(requestSpec, responseSpec, RETRIEVE_TRANSFER_URL);
-    }
-
-    public PageExternalTransferData retrieveTransferByLoanId(Long loanId) {
-        return ok(fineract().externalAssetOwners.getTransfer1(null, loanId, null, 0, 100));
-    }
-
-    public PageExternalTransferData retrieveTransferByLoanId(Long loanId, int offset, int limit) {
-        return ok(fineract().externalAssetOwners.getTransfer1(null, loanId, null, offset, limit));
-    }
-}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
index 8887bb386..a218ab0da 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/InitiateExternalAssetOwnerTransferTest.java
@@ -19,65 +19,79 @@
 package org.apache.fineract.integrationtests.investor.externalassetowner;
 
 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 com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
 import io.restassured.builder.RequestSpecBuilder;
 import io.restassured.builder.ResponseSpecBuilder;
 import io.restassured.http.ContentType;
 import io.restassured.path.json.JsonPath;
 import io.restassured.specification.RequestSpecification;
 import io.restassured.specification.ResponseSpecification;
-import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
+import java.util.UUID;
 import org.apache.fineract.client.models.ExternalTransferData;
 import org.apache.fineract.client.models.PageExternalTransferData;
+import org.apache.fineract.client.models.PostInitiateTransferRequest;
+import org.apache.fineract.client.models.PostInitiateTransferResponse;
+import org.apache.fineract.client.util.CallFailedRuntimeException;
 import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
 import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.BusinessStepHelper;
 import org.apache.fineract.integrationtests.common.ClientHelper;
 import org.apache.fineract.integrationtests.common.CollateralManagementHelper;
+import org.apache.fineract.integrationtests.common.ExternalAssetOwnerHelper;
 import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
+import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
 import org.apache.fineract.integrationtests.common.Utils;
 import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
 import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
 import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
 import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
+import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
 import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
 
+@ExtendWith(LoanTestLifecycleExtension.class)
 public class InitiateExternalAssetOwnerTransferTest {
 
     private ResponseSpecification responseSpec;
-    private ResponseSpecification responseSpecError;
-    private ResponseSpecification responseSpecNotFound;
     private RequestSpecification requestSpec;
     private ExternalAssetOwnerHelper externalAssetOwnerHelper;
     private LoanTransactionHelper loanTransactionHelper;
+    private SchedulerJobHelper schedulerJobHelper;
     private LocalDate todaysDate;
 
+    @BeforeAll
+    public static void setupInvestorBusinessStep() {
+        new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION",
+                "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+                "EXTERNAL_ASSET_OWNER_TRANSFER");
+    }
+
     @BeforeEach
     public void setup() {
         Utils.initializeRESTAssured();
         requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
         requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
         responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
-        responseSpecError = new ResponseSpecBuilder().expectStatusCode(403).build();
-        responseSpecNotFound = new ResponseSpecBuilder().expectStatusCode(404).build();
-        externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpec);
+        externalAssetOwnerHelper = new ExternalAssetOwnerHelper();
         loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
+        schedulerJobHelper = new SchedulerJobHelper(requestSpec);
 
         todaysDate = Utils.getLocalDateOfTenant();
     }
 
     @Test
-    public void saleActiveLoanToExternalAssetOwner() {
+    public void saleActiveLoanToExternalAssetOwnerAndBuybackADayLater() {
         try {
             GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
 
@@ -110,18 +124,96 @@ public class InitiateExternalAssetOwnerTransferTest {
                     JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
             LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
 
-            String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e";
-            String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
-                    getSaleRequestJson("04 March 2020", transferExternalId));
-            Type type = new TypeToken<Map<String, Object>>() {}.getType();
-            Map<String, Object> responseMap = new Gson().fromJson(saleResponse, type);
-            assertEquals(responseMap.get("resourceExternalId"), transferExternalId);
+            String transferExternalId = UUID.randomUUID().toString();
+            PostInitiateTransferResponse saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
+                    new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en")
+                            .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0"));
+            assertEquals(transferExternalId, saleResponse.getResourceExternalId());
 
             PageExternalTransferData retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
-            List<ExternalTransferData> retrieveResponseMap = retrieveResponse.getContent();
-            assertEquals(1, retrieveResponse.getTotalElements());
-            assertEquals(retrieveResponseMap.get(0).getTransferExternalId(), transferExternalId);
-            assertEquals(retrieveResponseMap.get(0).getStatus(), ExternalTransferData.StatusEnum.PENDING);
+
+            assertEquals(1, retrieveResponse.getNumberOfElements());
+            ExternalTransferData externalTransferData = retrieveResponse.getContent().get(0);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo());
+
+            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 3));
+            // Run the Loan COB Job
+            final String jobName = "Loan COB";
+            schedulerJobHelper.executeAndAwaitJob(jobName);
+
+            retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
+
+            assertEquals(2, retrieveResponse.getNumberOfElements());
+            externalTransferData = retrieveResponse.getContent().get(0);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(1);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.ACTIVE, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo());
+
+            String buybackTransferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e";
+            PostInitiateTransferResponse buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback",
+                    new PostInitiateTransferRequest().settlementDate("2020-03-03").dateFormat("yyyy-MM-dd").locale("en")
+                            .transferExternalId(buybackTransferExternalId));
+
+            assertEquals(buybackResponse.getResourceExternalId(), buybackTransferExternalId);
+
+            retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
+
+            assertEquals(3, retrieveResponse.getNumberOfElements());
+            externalTransferData = retrieveResponse.getContent().get(0);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(1);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.ACTIVE, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(2);
+            assertEquals(buybackTransferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo());
+
+            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 4));
+
+            schedulerJobHelper.executeAndAwaitJob(jobName);
+
+            retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
+            assertEquals(3, retrieveResponse.getNumberOfElements());
+            externalTransferData = retrieveResponse.getContent().get(0);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(1);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.ACTIVE, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(2);
+            assertEquals(buybackTransferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 3), externalTransferData.getEffectiveTo());
         } finally {
             requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
             requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
@@ -165,19 +257,17 @@ public class InitiateExternalAssetOwnerTransferTest {
                     JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
             LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
 
-            String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e";
-            String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
-                    getSaleRequestJson("04 March 2020", transferExternalId));
-            Type type = new TypeToken<Map<String, Object>>() {}.getType();
-            Map<String, Object> responseMap = new Gson().fromJson(saleResponse, type);
-            assertEquals(responseMap.get("resourceExternalId"), transferExternalId);
-
-            externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpecError);
-            String errorResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
-                    getSaleRequestJson("04 March 2020", transferExternalId));
-            Map<String, Object> errorResponseMap = new Gson().fromJson(errorResponse, type);
-            assertEquals("External asset owner transfer is already in PENDING state for this loan.",
-                    errorResponseMap.get("developerMessage"));
+            String transferExternalId = UUID.randomUUID().toString();
+            PostInitiateTransferResponse saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
+                    new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en")
+                            .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0"));
+            assertEquals(transferExternalId, saleResponse.getResourceExternalId());
+
+            CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
+                    () -> externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
+                            new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en")
+                                    .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0")));
+            assertTrue(exception.getMessage().contains("External asset owner transfer is already in PENDING state for this loan"));
         } finally {
             requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
             requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
@@ -226,13 +316,12 @@ public class InitiateExternalAssetOwnerTransferTest {
 
             loanTransactionHelper.makeRepayment("04 March 2020", 16000.0f, loanID);
 
-            externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpecError);
             String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e";
-            String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
-                    getSaleRequestJson("05 March 2020", transferExternalId));
-            Type type = new TypeToken<Map<String, Object>>() {}.getType();
-            Map<String, Object> errorResponseMap = new Gson().fromJson(saleResponse, type);
-            assertEquals("Loan is not in active status", errorResponseMap.get("developerMessage"));
+            CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
+                    () -> externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
+                            new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en")
+                                    .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0")));
+            assertTrue(exception.getMessage().contains("Loan is not in active status"));
         } finally {
             requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
             requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
@@ -278,58 +367,147 @@ public class InitiateExternalAssetOwnerTransferTest {
             LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
 
             String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e";
-            String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
-                    getSaleRequestJson("04 March 2020", transferExternalId));
-            Type type = new TypeToken<Map<String, Object>>() {}.getType();
-            Map<String, Object> responseMap = new Gson().fromJson(saleResponse, type);
-            assertEquals(responseMap.get("resourceExternalId"), transferExternalId);
-
-            String buybackTransferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e";
-            String buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback",
-                    getSaleRequestJson("04 March 2020", buybackTransferExternalId));
-            Map<String, Object> buybackResponseMap = new Gson().fromJson(buybackResponse, type);
-            assertEquals(buybackResponseMap.get("resourceExternalId"), buybackTransferExternalId);
+            PostInitiateTransferResponse saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
+                    new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en")
+                            .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0"));
+            assertEquals(transferExternalId, saleResponse.getResourceExternalId());
 
-            PageExternalTransferData retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue(), 0, 1);
-            List<ExternalTransferData> retrieveResponseMap = retrieveResponse.getContent();
+            PostInitiateTransferResponse buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback",
+                    new PostInitiateTransferRequest().settlementDate("2020-03-02").dateFormat("yyyy-MM-dd").locale("en")
+                            .transferExternalId(transferExternalId));
 
-            assertEquals(2, retrieveResponse.getTotalElements());
-            assertEquals(1, retrieveResponse.getNumberOfElements());
-            assertEquals(retrieveResponseMap.get(0).getTransferExternalId(), transferExternalId);
-            assertEquals(retrieveResponseMap.get(0).getStatus(), ExternalTransferData.StatusEnum.PENDING);
-
-            retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue(), 1, 1);
-            retrieveResponseMap = retrieveResponse.getContent();
+            assertEquals(buybackResponse.getResourceExternalId(), transferExternalId);
 
-            assertEquals(2, retrieveResponse.getTotalElements());
-            assertEquals(1, retrieveResponse.getNumberOfElements());
-            assertEquals(retrieveResponseMap.get(0).getTransferExternalId(), buybackTransferExternalId);
-            assertEquals(retrieveResponseMap.get(0).getStatus(), ExternalTransferData.StatusEnum.BUYBACK);
+            PageExternalTransferData retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
+            assertEquals(2, retrieveResponse.getNumberOfElements());
+            ExternalTransferData externalTransferData = retrieveResponse.getContent().get(0);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(1);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo());
+
+            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 3));
+            final String jobName = "Loan COB";
+            schedulerJobHelper.executeAndAwaitJob(jobName);
+
+            retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
+            assertEquals(4, retrieveResponse.getNumberOfElements());
+            externalTransferData = retrieveResponse.getContent().get(0);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(1);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(2);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.CANCELLED, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(3);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.CANCELLED, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveTo());
         } finally {
             requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
             requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
             requestSpec.header("Fineract-Platform-TenantId", "default");
             responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+            BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, todaysDate);
             GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE);
         }
     }
 
     @Test
-    public void getNotFoundErrorIfTheLoanDoesNotExistWithTheGivenID() {
+    public void saleAndBuybackMultipleTimes() {
         try {
             GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE);
 
             BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, LocalDate.of(2020, 3, 2));
             GlobalConfigurationHelper.updateValueForGlobalConfiguration(requestSpec, responseSpec, "10", "0");
 
-            externalAssetOwnerHelper = new ExternalAssetOwnerHelper(requestSpec, responseSpecNotFound);
+            final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec);
+            Assertions.assertNotNull(clientID);
+
+            Integer overdueFeeChargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
+                    ChargesHelper.getLoanOverdueFeeJSONWithCalculationTypePercentage("1"));
+            Assertions.assertNotNull(overdueFeeChargeId);
+
+            final Integer loanProductID = createLoanProduct(overdueFeeChargeId.toString());
+            Assertions.assertNotNull(loanProductID);
+            HashMap loanStatusHashMap;
+
+            final Integer loanID = applyForLoanApplication(clientID.toString(), loanProductID.toString(), null, "10 January 2020");
+
+            Assertions.assertNotNull(loanID);
+
+            loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
+            LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+            loanStatusHashMap = loanTransactionHelper.approveLoan("01 March 2020", loanID);
+            LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+
+            String loanDetails = loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID);
+            loanStatusHashMap = loanTransactionHelper.disburseLoanWithNetDisbursalAmount("02 March 2020", loanID,
+                    JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
+            LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
             String transferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e";
-            String nonExistingLoanExternalID = "NonExistingLoanExternalID";
-            String saleResponse = externalAssetOwnerHelper.initiateTransferByLoanExternalId(nonExistingLoanExternalID, "sale",
-                    getSaleRequestJson("05 March 2020", transferExternalId));
-            Type type = new TypeToken<Map<String, Object>>() {}.getType();
-            Map<String, Object> errorResponseMap = new Gson().fromJson(saleResponse, type);
-            assertEquals("The requested resource is not available.", errorResponseMap.get("developerMessage"));
+            PostInitiateTransferResponse saleResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
+                    new PostInitiateTransferRequest().settlementDate("2020-03-04").dateFormat("yyyy-MM-dd").locale("en")
+                            .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0"));
+
+            assertEquals(transferExternalId, saleResponse.getResourceExternalId());
+
+            String buybackTransferExternalId = "36efeb06-d835-48a1-99eb-09bd1d348c1e";
+            PostInitiateTransferResponse buybackResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback",
+                    new PostInitiateTransferRequest().settlementDate("2020-03-04").dateFormat("yyyy-MM-dd").locale("en")
+                            .transferExternalId(buybackTransferExternalId));
+
+            assertEquals(buybackResponse.getResourceExternalId(), buybackTransferExternalId);
+            PageExternalTransferData retrieveResponse = externalAssetOwnerHelper.retrieveTransferByLoanId(loanID.longValue());
+            assertEquals(2, retrieveResponse.getNumberOfElements());
+            ExternalTransferData externalTransferData = retrieveResponse.getContent().get(0);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.PENDING, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 4), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo());
+            externalTransferData = retrieveResponse.getContent().get(1);
+            assertEquals(transferExternalId, externalTransferData.getTransferExternalId());
+            assertEquals(ExternalTransferData.StatusEnum.BUYBACK, externalTransferData.getStatus());
+            assertEquals(LocalDate.of(2020, 3, 4), externalTransferData.getSettlementDate());
+            assertEquals(LocalDate.of(2020, 3, 2), externalTransferData.getEffectiveFrom());
+            assertEquals(LocalDate.of(9999, 12, 31), externalTransferData.getEffectiveTo());
+
+            CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
+                    () -> externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "sale",
+                            new PostInitiateTransferRequest().settlementDate("2020-03-04").dateFormat("yyyy-MM-dd").locale("en")
+                                    .transferExternalId(transferExternalId).ownerExternalId("1234567890").purchasePriceRatio("1.0")));
+
+            assertTrue(exception.getMessage().contains("This loan cannot be sold, there is already an in progress transfer"));
+
+            CallFailedRuntimeException exception2 = assertThrows(CallFailedRuntimeException.class,
+                    () -> externalAssetOwnerHelper.initiateTransferByLoanId(loanID.longValue(), "buyback",
+                            new PostInitiateTransferRequest().settlementDate("2020-03-04").dateFormat("yyyy-MM-dd").locale("en")
+                                    .transferExternalId(buybackTransferExternalId)));
+            assertTrue(exception2.getMessage()
+                    .contains("This loan cannot be bought back, external asset owner buyback transfer is already in progress"));
         } finally {
             requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
             requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
@@ -378,15 +556,4 @@ public class InitiateExternalAssetOwnerTransferTest {
         return collateral;
     }
 
-    private String getSaleRequestJson(String date, String transferExternalId) {
-        final HashMap<String, String> map = new HashMap<>();
-        map.put("settlementDate", date);
-        map.put("ownerExternalId", "1234567890987654321");
-        map.put("transferExternalId", transferExternalId);
-        map.put("purchasePriceRatio", "1.234");
-        map.put("dateFormat", "dd MMMM yyyy");
-        map.put("locale", "en");
-        return new Gson().toJson(map);
-    }
-
 }