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

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


The following commit(s) were added to refs/heads/develop by this push:
     new d639b20af FINERACT-1926: Asset transfer to external owner COB business 
step
d639b20af is described below

commit d639b20af16c61ebdec168cad29c78f88ab94405
Author: Adam Saghy <[email protected]>
AuthorDate: Fri May 26 16:40:28 2023 +0200

    FINERACT-1926: Asset transfer to external owner COB business step
---
 .../cob/exceptions/BusinessStepException.java      |   0
 .../loan/LoanAccountOwnerTransferBusinessStep.java | 195 +++++++++++++
 .../investor/data/ExternalTransferSubStatus.java   |  13 +-
 .../domain/ExternalAssetOwnerTransfer.java         |  27 +-
 .../ExternalAssetOwnerTransferLoanMapping.java     |   5 +-
 ...alAssetOwnerTransferLoanMappingRepository.java} |  11 +-
 .../ExternalAssetOwnerTransferRepository.java      |   1 +
 ...xternalAssetOwnerTransferNotFoundException.java |  33 +++
 .../BuybackLoanFromExternalAssetOwnerHandler.java  |   2 +-
 .../service/ExternalAssetOwnersReadService.java    |   1 +
 .../service/ExternalAssetOwnersWriteService.java   |   3 +-
 .../ExternalAssetOwnersWriteServiceImpl.java       |  14 +-
 .../module/investor/module-changelog-master.xml    |   1 +
 .../module/investor/parts/0006_asset_schemas.xml   |  49 ++++
 .../LoanAccountOwnerTransferBusinessStepTest.java  | 307 +++++++++++++++++++++
 15 files changed, 628 insertions(+), 34 deletions(-)

diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
 
b/fineract-core/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
similarity index 100%
copy from 
fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
copy to 
fineract-core/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
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
new file mode 100644
index 000000000..e347aeb3d
--- /dev/null
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStep.java
@@ -0,0 +1,195 @@
+/**
+ * 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.investor.cob.loan;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.cob.loan.LoanCOBBusinessStep;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.investor.data.ExternalTransferStatus;
+import org.apache.fineract.investor.data.ExternalTransferSubStatus;
+import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
+import 
org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
+import 
org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
+import 
org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class LoanAccountOwnerTransferBusinessStep implements 
LoanCOBBusinessStep {
+
+    public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 
12, 31);
+    private final ExternalAssetOwnerTransferRepository 
externalAssetOwnerTransferRepository;
+    private final ExternalAssetOwnerTransferLoanMappingRepository 
externalAssetOwnerTransferLoanMappingRepository;
+
+    @Override
+    public Loan execute(Loan loan) {
+        Long loanId = loan.getId();
+        log.debug("start processing loan ownership transfer business step for 
loan with Id [{}]", loanId);
+
+        LocalDate settlementDate = DateUtils.getBusinessLocalDate();
+        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))),
+                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) {
+            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));
+            }
+        } 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()));
+            }
+            handleSale(loan, settlementDate, externalAssetOwnerTransfer);
+        }
+
+        log.debug("end processing loan ownership transfer business step for 
loan Id [{}]", loan.getId());
+        return loan;
+    }
+
+    private void handleSale(final Loan loan, final LocalDate settlementDate, 
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
+        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = 
sellAsset(loan, settlementDate, externalAssetOwnerTransfer);
+        // TODO: trigger asset loan transfer executed event
+    }
+
+    private void handleBuyback(final Loan loan, final LocalDate settlementDate,
+            final List<ExternalAssetOwnerTransfer> 
externalAssetOwnerTransferList) {
+        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = 
buybackAsset(loan, settlementDate, externalAssetOwnerTransferList);
+        // 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);
+        
buybackExternalAssetOwnerTransfer.setEffectiveDateTo(buybackExternalAssetOwnerTransfer.getEffectiveDateFrom());
+        
externalAssetOwnerTransferRepository.save(saleExternalAssetOwnerTransfer);
+        buybackExternalAssetOwnerTransfer = 
externalAssetOwnerTransferRepository.save(buybackExternalAssetOwnerTransfer);
+        
externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(),
 buybackExternalAssetOwnerTransfer);
+        // TODO: create asset ownership accounting entries
+        // TODO: create asset ownership transaction entries
+        return buybackExternalAssetOwnerTransfer;
+    }
+
+    private ExternalAssetOwnerTransfer sellAsset(final Loan loan, final 
LocalDate settlementDate,
+            ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
+        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer;
+        if (isTransferable(loan)) {
+            newExternalAssetOwnerTransfer = createActiveEntry(settlementDate, 
externalAssetOwnerTransfer);
+            createActiveMapping(loan.getId(), newExternalAssetOwnerTransfer);
+            // TODO: create asset ownership accounting entries
+            // TODO: create asset ownership transaction entries
+        } else {
+            ExternalTransferSubStatus subStatus = 
ExternalTransferSubStatus.BALANCE_ZERO;
+            if (loan.getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0) {
+                subStatus = ExternalTransferSubStatus.BALANCE_NEGATIVE;
+            }
+            newExternalAssetOwnerTransfer = createNewEntry(settlementDate, 
externalAssetOwnerTransfer, ExternalTransferStatus.DECLINED,
+                    subStatus, settlementDate, settlementDate);
+        }
+        return newExternalAssetOwnerTransfer;
+    }
+
+    private void createActiveMapping(Long loanId, ExternalAssetOwnerTransfer 
externalAssetOwnerTransfer) {
+        ExternalAssetOwnerTransferLoanMapping 
externalAssetOwnerTransferLoanMapping = new 
ExternalAssetOwnerTransferLoanMapping();
+        externalAssetOwnerTransferLoanMapping.setLoanId(loanId);
+        
externalAssetOwnerTransferLoanMapping.setOwnerTransfer(externalAssetOwnerTransfer);
+        
externalAssetOwnerTransferLoanMappingRepository.save(externalAssetOwnerTransferLoanMapping);
+    }
+
+    private boolean isTransferable(final Loan loan) {
+        return 
loan.getLoanSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0;
+    }
+
+    private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, 
final List<ExternalAssetOwnerTransfer> transferDataList) {
+        ExternalAssetOwnerTransfer cancelledPendingTransfer = 
cancelTransfer(settlementDate, transferDataList.get(0));
+        ExternalAssetOwnerTransfer cancelledBuybackTransfer = 
cancelTransfer(settlementDate, transferDataList.get(1));
+        // TODO: trigger asset loan transfer cancel events
+    }
+
+    private ExternalAssetOwnerTransfer cancelTransfer(final LocalDate 
settlementDate,
+            final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
+        return createNewEntry(settlementDate, externalAssetOwnerTransfer, 
ExternalTransferStatus.CANCELLED,
+                ExternalTransferSubStatus.SAMEDAY_TRANSFERS, settlementDate, 
settlementDate);
+    }
+
+    private ExternalAssetOwnerTransfer createNewEntry(final LocalDate 
settlementDate,
+            final ExternalAssetOwnerTransfer externalAssetOwnerTransfer, final 
ExternalTransferStatus status,
+            final ExternalTransferSubStatus subStatus, final LocalDate 
effectiveDateFrom, final LocalDate effectiveDateTo) {
+        ExternalAssetOwnerTransfer newExternalAssetOwnerTransfer = new 
ExternalAssetOwnerTransfer();
+        
newExternalAssetOwnerTransfer.setOwner(externalAssetOwnerTransfer.getOwner());
+        
newExternalAssetOwnerTransfer.setExternalId(externalAssetOwnerTransfer.getExternalId());
+        newExternalAssetOwnerTransfer.setStatus(status);
+        newExternalAssetOwnerTransfer.setSubStatus(subStatus);
+        newExternalAssetOwnerTransfer.setSettlementDate(settlementDate);
+        
newExternalAssetOwnerTransfer.setLoanId(externalAssetOwnerTransfer.getLoanId());
+        
newExternalAssetOwnerTransfer.setExternalLoanId(externalAssetOwnerTransfer.getExternalLoanId());
+        
newExternalAssetOwnerTransfer.setPurchasePriceRatio(externalAssetOwnerTransfer.getPurchasePriceRatio());
+        newExternalAssetOwnerTransfer.setEffectiveDateFrom(effectiveDateFrom);
+        newExternalAssetOwnerTransfer.setEffectiveDateTo(effectiveDateTo);
+
+        externalAssetOwnerTransfer.setEffectiveDateTo(settlementDate);
+        externalAssetOwnerTransferRepository.save(externalAssetOwnerTransfer);
+        return 
externalAssetOwnerTransferRepository.save(newExternalAssetOwnerTransfer);
+    }
+
+    private ExternalAssetOwnerTransfer createActiveEntry(final LocalDate 
settlementDate,
+            final ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
+        LocalDate effectiveFrom = settlementDate.plusDays(1);
+        return createNewEntry(settlementDate, externalAssetOwnerTransfer, 
ExternalTransferStatus.ACTIVE, null, effectiveFrom,
+                FUTURE_DATE_9999_12_31);
+    }
+
+    @Override
+    public String getEnumStyledName() {
+        return "EXTERNAL_ASSET_OWNER_TRANSFER";
+    }
+
+    @Override
+    public String getHumanReadableName() {
+        return "Execute external asset owner transfer";
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java
similarity index 73%
rename from 
fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
rename to 
fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java
index a11a5c57a..7899b49a8 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/exceptions/BusinessStepException.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/data/ExternalTransferSubStatus.java
@@ -16,15 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.exceptions;
+package org.apache.fineract.investor.data;
 
-public class BusinessStepException extends RuntimeException {
-
-    public BusinessStepException(String message) {
-        super(message);
-    }
-
-    public BusinessStepException(String message, Throwable t) {
-        super(message, t);
-    }
+public enum ExternalTransferSubStatus {
+    BALANCE_ZERO, BALANCE_NEGATIVE, SAMEDAY_TRANSFERS, USER_REQUESTED
 }
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java
index 618c11ecb..2179dbff1 100644
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransfer.java
@@ -21,6 +21,8 @@ package org.apache.fineract.investor.domain;
 import java.time.LocalDate;
 import javax.persistence.Column;
 import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
 import javax.persistence.JoinColumn;
 import javax.persistence.ManyToOne;
 import javax.persistence.Table;
@@ -29,6 +31,8 @@ import lombok.NoArgsConstructor;
 import lombok.Setter;
 import 
org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
 import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import org.apache.fineract.investor.data.ExternalTransferStatus;
+import org.apache.fineract.investor.data.ExternalTransferSubStatus;
 
 @Getter
 @Setter
@@ -37,29 +41,34 @@ import 
org.apache.fineract.infrastructure.core.domain.ExternalId;
 @Entity
 public class ExternalAssetOwnerTransfer extends 
AbstractAuditableWithUTCDateTimeCustom {
 
-    @Column(name = "owner_id")
+    @Column(name = "owner_id", nullable = false)
     private Long ownerId;
 
     @ManyToOne
-    @JoinColumn(name = "owner_id", insertable = false, updatable = false)
+    @JoinColumn(name = "owner_id", insertable = false, updatable = false, 
nullable = false)
     private ExternalAssetOwner owner;
 
-    @Column(name = "external_id", length = 100)
+    @Column(name = "external_id", length = 100, nullable = false)
     private ExternalId externalId;
 
-    @Column(name = "status", length = 50)
-    private String status;
+    @Column(name = "status", length = 50, nullable = false)
+    @Enumerated(EnumType.STRING)
+    private ExternalTransferStatus status;
 
-    @Column(name = "purchase_price_ratio", length = 50)
+    @Column(name = "sub_status", length = 50)
+    @Enumerated(EnumType.STRING)
+    private ExternalTransferSubStatus subStatus;
+
+    @Column(name = "purchase_price_ratio", length = 50, nullable = false)
     private String purchasePriceRatio;
 
-    @Column(name = "settlement_date")
+    @Column(name = "settlement_date", nullable = false)
     private LocalDate settlementDate;
 
-    @Column(name = "effective_date_from")
+    @Column(name = "effective_date_from", nullable = false)
     private LocalDate effectiveDateFrom;
 
-    @Column(name = "effective_date_to")
+    @Column(name = "effective_date_to", nullable = false)
     private LocalDate effectiveDateTo;
 
     @Column(name = "loan_id", nullable = false)
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMapping.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMapping.java
index f94683a24..eef0b33dc 100644
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMapping.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMapping.java
@@ -23,7 +23,6 @@ import javax.persistence.Entity;
 import javax.persistence.JoinColumn;
 import javax.persistence.ManyToOne;
 import javax.persistence.Table;
-import lombok.AccessLevel;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
@@ -32,7 +31,7 @@ import 
org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDa
 @Getter
 @Setter
 @Table(name = "m_external_asset_owner_transfer_loan_mapping")
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@NoArgsConstructor
 @Entity
 public class ExternalAssetOwnerTransferLoanMapping extends 
AbstractAuditableWithUTCDateTimeCustom {
 
@@ -40,7 +39,7 @@ public class ExternalAssetOwnerTransferLoanMapping extends 
AbstractAuditableWith
     private Long loanId;
 
     @ManyToOne
-    @JoinColumn(name = "owner_transfer_id")
+    @JoinColumn(name = "owner_transfer_id", nullable = false)
     private ExternalAssetOwnerTransfer ownerTransfer;
 
 }
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java
similarity index 61%
copy from 
fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
copy to 
fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java
index 8f5f56d85..e7b28b4d8 100644
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/domain/ExternalAssetOwnerTransferLoanMappingRepository.java
@@ -16,12 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.investor.service;
+package org.apache.fineract.investor.domain;
 
-import java.util.List;
-import org.apache.fineract.investor.data.ExternalTransferData;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
-public interface ExternalAssetOwnersReadService {
+public interface ExternalAssetOwnerTransferLoanMappingRepository extends 
JpaRepository<ExternalAssetOwnerTransferLoanMapping, Long>,
+        JpaSpecificationExecutor<ExternalAssetOwnerTransferLoanMapping> {
 
-    List<ExternalTransferData> retrieveTransferData(Long loanId, String 
externalLoanId, String transferExternalId);
+    void deleteByLoanIdAndOwnerTransfer(Long loanId, 
ExternalAssetOwnerTransfer externalAssetOwnerTransfer);
 }
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 799143d0a..d6fe00f09 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
@@ -37,4 +37,5 @@ 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);
+
 }
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java
new file mode 100644
index 000000000..985517a4d
--- /dev/null
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/exception/ExternalAssetOwnerTransferNotFoundException.java
@@ -0,0 +1,33 @@
+/**
+ * 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.investor.exception;
+
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
+import org.apache.fineract.investor.data.ExternalTransferStatus;
+
+public class ExternalAssetOwnerTransferNotFoundException extends 
AbstractPlatformResourceNotFoundException {
+
+    public ExternalAssetOwnerTransferNotFoundException(ExternalId externalId, 
ExternalTransferStatus externalTransferStatus) {
+        super("error.msg.external.asset.owner.transfer.external.id.and.status",
+                String.format("External asset owner transfer with external id: 
%s and status: %s does not found", externalId.getValue(),
+                        externalTransferStatus),
+                externalId.getValue(), externalTransferStatus);
+    }
+}
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java
index 9fc11ca38..00977500c 100644
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/BuybackLoanFromExternalAssetOwnerHandler.java
@@ -34,6 +34,6 @@ public class BuybackLoanFromExternalAssetOwnerHandler 
implements NewCommandSourc
 
     @Override
     public CommandProcessingResult processCommand(JsonCommand command) {
-        return externalAssetOwnersWriteService.buyBackLoanByLoanId(command);
+        return externalAssetOwnersWriteService.buybackLoanByLoanId(command);
     }
 }
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
index 8f5f56d85..bf011d6ca 100644
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersReadService.java
@@ -24,4 +24,5 @@ import org.apache.fineract.investor.data.ExternalTransferData;
 public interface ExternalAssetOwnersReadService {
 
     List<ExternalTransferData> retrieveTransferData(Long loanId, String 
externalLoanId, String transferExternalId);
+
 }
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
index b3133875d..ac73af9b9 100644
--- 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteService.java
@@ -25,5 +25,6 @@ public interface ExternalAssetOwnersWriteService {
 
     CommandProcessingResult saleLoanByLoanId(JsonCommand command);
 
-    CommandProcessingResult buyBackLoanByLoanId(JsonCommand command);
+    CommandProcessingResult buybackLoanByLoanId(JsonCommand command);
+
 }
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 043cdada8..1dc7c4196 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
@@ -50,6 +50,7 @@ import 
org.apache.fineract.investor.data.ExternalTransferStatus;
 import org.apache.fineract.investor.domain.ExternalAssetOwner;
 import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository;
 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;
@@ -61,6 +62,7 @@ import 
org.springframework.transaction.annotation.Transactional;
 public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersWriteService {
 
     private final ExternalAssetOwnerTransferRepository 
externalAssetOwnerTransferRepository;
+    private final ExternalAssetOwnerTransferLoanMappingRepository 
externalAssetOwnerTransferLoanMappingRepository;
     private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
     private final FromJsonHelper fromApiJsonHelper;
     private final LoanReadPlatformServiceCommon loanReadPlatformService;
@@ -80,7 +82,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
 
     @Override
     @Transactional
-    public CommandProcessingResult buyBackLoanByLoanId(JsonCommand command) {
+    public CommandProcessingResult buybackLoanByLoanId(JsonCommand command) {
         Long loanId = command.getLoanId();
         LoanIdAndExternalIdData loanIdAndExternalId = 
loanReadPlatformService.getTransferableLoanIdAndExternalId(loanId);
         validateLoanStatus(loanIdAndExternalId);
@@ -135,7 +137,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
                 .findLatestByLoanId(externalAssetOwnerTransfer.getLoanId());
         if (latestTransferOptional.isPresent()) {
             ExternalAssetOwnerTransfer latestTransfer = 
latestTransferOptional.get();
-            ExternalTransferStatus latestTransferStatus = 
ExternalTransferStatus.valueOf(latestTransfer.getStatus());
+            ExternalTransferStatus latestTransferStatus = 
latestTransfer.getStatus();
             if (latestTransferStatus.equals(ExternalTransferStatus.PENDING)) {
                 throw new ExternalAssetOwnerInitiateTransferException(
                         "External asset owner transfer is already in PENDING 
state for this loan.");
@@ -154,7 +156,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
                     "This loan cannot be bought back, because it is not owned 
by an external asset owner");
         } else {
             ExternalAssetOwnerTransfer latestTransfer = 
latestTransferOptional.get();
-            ExternalTransferStatus latestTransferStatus = 
ExternalTransferStatus.valueOf(latestTransfer.getStatus());
+            ExternalTransferStatus latestTransferStatus = 
latestTransfer.getStatus();
             if (latestTransferStatus.equals(ExternalTransferStatus.BUYBACK)) {
                 throw new ExternalAssetOwnerInitiateTransferException(
                         "External asset owner transfer is already in BUYBACK 
state for this loan.");
@@ -173,7 +175,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
         externalAssetOwnerTransfer.setOwnerId(owner.getId());
         externalAssetOwnerTransfer.setOwner(owner);
         
externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json));
-        externalAssetOwnerTransfer.setStatus(status.name());
+        externalAssetOwnerTransfer.setStatus(status);
         
externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json));
         
externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json));
         
externalAssetOwnerTransfer.setEffectiveDateFrom(ThreadLocalContextUtil.getBusinessDate());
@@ -188,7 +190,9 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
                 
Arrays.asList(ExternalTransferRequestParameters.SETTLEMENT_DATE, 
ExternalTransferRequestParameters.OWNER_EXTERNAL_ID,
                         
ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, 
ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO,
                         ExternalTransferRequestParameters.DATEFORMAT, 
ExternalTransferRequestParameters.LOCALE));
-        final Type typeOfMap = new TypeToken<Map<String, Object>>() 
{}.getType();
+        final Type typeOfMap = new TypeToken<Map<String, Object>>() {
+
+        }.getType();
         fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, 
apiRequestBodyAsJson, requestParameters);
 
         final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
diff --git 
a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml
 
b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml
index c7fa915cc..6ce35e811 100644
--- 
a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml
+++ 
b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml
@@ -27,4 +27,5 @@
   <include relativeToChangelogFile="true" file="parts/0003_asset_schemas.xml"/>
   <include relativeToChangelogFile="true" 
file="parts/0004_change_purchase_price_ratio_type.xml"/>
   <include relativeToChangelogFile="true" 
file="parts/0005_add_sale_and_buyback_command.xml"/>
+  <include relativeToChangelogFile="true" file="parts/0006_asset_schemas.xml"/>
 </databaseChangeLog>
diff --git 
a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0006_asset_schemas.xml
 
b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0006_asset_schemas.xml
new file mode 100644
index 000000000..b35a8cf3b
--- /dev/null
+++ 
b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0006_asset_schemas.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements. See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership. The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied. See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog";
+                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+                   
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog 
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd";>
+
+    <changeSet author="fineract" id="1">
+        <addColumn tableName="m_external_asset_owner_transfer">
+            <column name="sub_status" type="VARCHAR(50)"/>
+        </addColumn>
+
+        <createIndex tableName="m_external_asset_owner_transfer"
+                     indexName="external_asset_owner_transfer_sub_status">
+            <column name="sub_status"/>
+        </createIndex>
+    </changeSet>
+    <changeSet id="2" author="fineract">
+        <addNotNullConstraint tableName="m_external_asset_owner_transfer" 
columnName="owner_id" columnDataType="BIGINT"/>
+        <addNotNullConstraint tableName="m_external_asset_owner_transfer" 
columnName="external_id" columnDataType="VARCHAR(100)"/>
+        <addNotNullConstraint tableName="m_external_asset_owner_transfer" 
columnName="status" columnDataType="VARCHAR(50)"/>
+        <addNotNullConstraint tableName="m_external_asset_owner_transfer" 
columnName="purchase_price_ratio" columnDataType="VARCHAR(50)"/>
+        <addNotNullConstraint tableName="m_external_asset_owner_transfer" 
columnName="settlement_date" columnDataType="DATE"/>
+        <addNotNullConstraint tableName="m_external_asset_owner_transfer" 
columnName="effective_date_from" columnDataType="DATE"/>
+        <addNotNullConstraint tableName="m_external_asset_owner_transfer" 
columnName="effective_date_to" columnDataType="DATE"/>
+    </changeSet>
+
+    <changeSet id="3" author="fineract">
+        <addNotNullConstraint 
tableName="m_external_asset_owner_transfer_loan_mapping" 
columnName="owner_transfer_id" columnDataType="BIGINT"/>
+    </changeSet>
+</databaseChangeLog>
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
new file mode 100644
index 000000000..e4f8e7d9c
--- /dev/null
+++ 
b/fineract-investor/src/test/java/org/apache/fineract/investor/cob/loan/LoanAccountOwnerTransferBusinessStepTest.java
@@ -0,0 +1,307 @@
+/**
+ * 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.investor.cob.loan;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.domain.ActionContext;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.investor.data.ExternalTransferStatus;
+import org.apache.fineract.investor.data.ExternalTransferSubStatus;
+import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
+import 
org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
+import 
org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
+import 
org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+
+@ExtendWith(MockitoExtension.class)
+public class LoanAccountOwnerTransferBusinessStepTest {
+
+    public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 
12, 31);
+    private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
+    @Mock
+    private ExternalAssetOwnerTransferRepository 
externalAssetOwnerTransferRepository;
+    @Mock
+    private ExternalAssetOwnerTransferLoanMappingRepository 
externalAssetOwnerTransferLoanMappingRepository;
+    private LoanAccountOwnerTransferBusinessStep underTest;
+
+    @BeforeEach
+    public void setUp() {
+        ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, 
"default", "Default", "Asia/Kolkata", null));
+        ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        ThreadLocalContextUtil.setBusinessDates(new 
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
+        underTest = new 
LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository,
+                externalAssetOwnerTransferLoanMappingRepository);
+    }
+
+    @Test
+    public void givenLoanNoTransfer() {
+        // 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);
+        // then
+        verify(externalAssetOwnerTransferRepository, 
times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, 
"id")));
+        assertEquals(processedLoan, loanForProcessing);
+    }
+
+    @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() {
+        // 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(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"))))
+                .thenReturn(response);
+        // 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());
+        verify(externalAssetOwnerTransferRepository, 
times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, 
"id")));
+    }
+
+    @Test
+    public void givenLoanTwoTransferSameDay() {
+        // 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.BUYBACK);
+        List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, 
secondResponseItem);
+        
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), 
eq(Sort.by(Sort.Direction.ASC, "id"))))
+                .thenReturn(response);
+        ArgumentCaptor<ExternalAssetOwnerTransfer> 
externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
+                .forClass(ExternalAssetOwnerTransfer.class);
+        // 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(4)).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.CANCELLED, 
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
+        assertEquals(ExternalTransferSubStatus.SAMEDAY_TRANSFERS,
+                
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSubStatus());
+        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(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2).getOwner(),
+                
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getOwner());
+        
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2).getExternalId(),
+                
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getExternalId());
+        assertEquals(ExternalTransferStatus.CANCELLED, 
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getStatus());
+        assertEquals(ExternalTransferSubStatus.SAMEDAY_TRANSFERS,
+                
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getSubStatus());
+        assertEquals(actualDate, 
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getSettlementDate());
+        
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2).getLoanId(),
+                
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getLoanId());
+        
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2).getPurchasePriceRatio(),
+                
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getPurchasePriceRatio());
+        assertEquals(actualDate, 
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getEffectiveDateFrom());
+        assertEquals(actualDate, 
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(3).getEffectiveDateTo());
+
+        assertEquals(processedLoan, loanForProcessing);
+    }
+
+    @Test
+    public void givenLoanBuyback() {
+        // 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.ACTIVE);
+        
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);
+        ArgumentCaptor<ExternalAssetOwnerTransfer> 
externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
+                .forClass(ExternalAssetOwnerTransfer.class);
+        
when(externalAssetOwnerTransferRepository.save(firstResponseItem)).thenReturn(firstResponseItem);
+        
when(externalAssetOwnerTransferRepository.save(secondResponseItem)).thenReturn(secondResponseItem);
+        // 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());
+        
verify(secondResponseItem).setEffectiveDateTo(secondResponseItem.getEffectiveDateFrom());
+        verify(externalAssetOwnerTransferLoanMappingRepository, 
times(1)).deleteByLoanIdAndOwnerTransfer(1L, secondResponseItem);
+
+        assertEquals(processedLoan, loanForProcessing);
+    }
+
+    @Test
+    public void givenLoanOneTransferButInvalidTransfer() {
+        // 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);
+        List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
+        
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, found: 
ACTIVE", exception.getMessage());
+        verify(externalAssetOwnerTransferRepository, 
times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, 
"id")));
+    }
+
+    @Test
+    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.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
+        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.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 testGetEnumStyledNameSuccessScenario() {
+        final String actualEnumName = underTest.getEnumStyledName();
+        assertNotNull(actualEnumName);
+        assertEquals("EXTERNAL_ASSET_OWNER_TRANSFER", actualEnumName);
+    }
+
+    @Test
+    public void testGetHumanReadableNameSuccessScenario() {
+        final String actualEnumName = underTest.getHumanReadableName();
+        assertNotNull(actualEnumName);
+        assertEquals("Execute external asset owner transfer", actualEnumName);
+    }
+}

Reply via email to