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 61915bbcc FINERACT-1724: Asset externalization of non-active loans
61915bbcc is described below

commit 61915bbcc487ac332e47deb67ef79a165cbcf36a
Author: abraham.menyhart <[email protected]>
AuthorDate: Tue Jul 18 11:44:03 2023 +0200

    FINERACT-1724: Asset externalization of non-active loans
---
 .../api/GlobalConfigurationApiConstant.java        |   0
 .../data/GlobalConfigurationData.java              |   0
 .../data/GlobalConfigurationDataValidator.java     |   0
 .../data/GlobalConfigurationPropertyData.java      |   0
 .../service/ConfigurationReadPlatformService.java  |   0
 ...lAssetOwnerLoanStatusChangePlatformService.java |  20 +-
 ...etOwnerLoanStatusChangePlatformServiceImpl.java |  57 ++++++
 .../ExternalAssetOwnersWriteServiceImpl.java       |  19 +-
 .../service/LoanAccountOwnerTransferService.java   |  15 +-
 .../LoanAccountOwnerTransferServiceImpl.java       | 208 +++++++++++++++++++++
 .../LoanAccountOwnerTransferServiceTest.java       | 179 ++++++++++++++++++
 .../loan/LoanStatusChangedBusinessEvent.java       |   0
 .../loanaccount/domain/LoanRepository.java         |   1 -
 .../db/changelog/tenant/changelog-tenant.xml       |   1 +
 ...n_asset_externalization_of_non_active_loans.xml |  41 ++++
 .../common/GlobalConfigurationHelper.java          |  14 +-
 .../InitiateExternalAssetOwnerTransferTest.java    | 143 +++++++++++++-
 17 files changed, 650 insertions(+), 48 deletions(-)

diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationApiConstant.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationApiConstant.java
similarity index 100%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationApiConstant.java
rename to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationApiConstant.java
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationData.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationData.java
similarity index 100%
copy from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationData.java
copy to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationData.java
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationDataValidator.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationDataValidator.java
similarity index 100%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationDataValidator.java
rename to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationDataValidator.java
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationPropertyData.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationPropertyData.java
similarity index 100%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationPropertyData.java
rename to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationPropertyData.java
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ConfigurationReadPlatformService.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/service/ConfigurationReadPlatformService.java
similarity index 100%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ConfigurationReadPlatformService.java
rename to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/service/ConfigurationReadPlatformService.java
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationData.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformService.java
similarity index 64%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationData.java
rename to 
fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformService.java
index bf65c8bb2..eb97c6844 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationData.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformService.java
@@ -16,22 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.configuration.data;
+package org.apache.fineract.investor.service;
 
-import java.util.List;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.Accessors;
-
-/**
- * Immutable data object for global configuration.
- */
-
-@Data
-@NoArgsConstructor
-@Accessors(chain = true)
-public class GlobalConfigurationData {
-
-    @SuppressWarnings("unused")
-    private List<GlobalConfigurationPropertyData> globalConfiguration;
-}
+public interface ExternalAssetOwnerLoanStatusChangePlatformService {}
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformServiceImpl.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformServiceImpl.java
new file mode 100644
index 000000000..352be35f8
--- /dev/null
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformServiceImpl.java
@@ -0,0 +1,57 @@
+/**
+ * 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.service;
+
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import 
org.apache.fineract.infrastructure.configuration.service.ConfigurationReadPlatformService;
+import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class ExternalAssetOwnerLoanStatusChangePlatformServiceImpl implements 
ExternalAssetOwnerLoanStatusChangePlatformService {
+
+    private final BusinessEventNotifierService businessEventNotifierService;
+    private final ConfigurationReadPlatformService 
configurationReadPlatformService;
+    private final LoanAccountOwnerTransferService 
loanAccountOwnerTransferService;
+
+    private static final String ASSET_EXTERNALIZATION_OF_NON_ACTIVE_LOANS = 
"asset-externalization-of-non-active-loans";
+
+    @PostConstruct
+    public void addListeners() {
+        
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class,
+                new ExternalAssetOwnerLoanStatusChangedListener());
+    }
+
+    private class ExternalAssetOwnerLoanStatusChangedListener implements 
BusinessEventListener<LoanStatusChangedBusinessEvent> {
+
+        @Override
+        public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
+            final Loan loan = event.get();
+            if 
(configurationReadPlatformService.retrieveGlobalConfiguration(ASSET_EXTERNALIZATION_OF_NON_ACTIVE_LOANS).isEnabled()
+                    && (loan.isClosed() || loan.getStatus().isOverpaid())) {
+                
loanAccountOwnerTransferService.handleLoanClosedOrOverpaid(loan);
+            }
+        }
+    }
+}
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 a56fca359..a9a4d24d9 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
@@ -18,6 +18,11 @@
  */
 package org.apache.fineract.investor.service;
 
+import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
+import static 
org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.ACTIVE;
+import static 
org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.TRANSFER_IN_PROGRESS;
+import static 
org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.TRANSFER_ON_HOLD;
+
 import com.google.gson.JsonElement;
 import com.google.gson.reflect.TypeToken;
 import java.lang.reflect.Type;
@@ -45,7 +50,6 @@ 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.infrastructure.event.business.service.BusinessEventNotifierService;
 import org.apache.fineract.investor.data.ExternalTransferRequestParameters;
 import org.apache.fineract.investor.data.ExternalTransferStatus;
 import org.apache.fineract.investor.data.ExternalTransferSubStatus;
@@ -65,15 +69,13 @@ import 
org.springframework.transaction.annotation.Transactional;
 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<LoanStatus> ACTIVE_LOAN_STATUSES = 
List.of(ACTIVE, TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD);
     private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES = 
List.of(ExternalTransferStatus.PENDING,
             ExternalTransferStatus.ACTIVE);
     private final ExternalAssetOwnerTransferRepository 
externalAssetOwnerTransferRepository;
     private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
     private final FromJsonHelper fromApiJsonHelper;
     private final LoanRepository loanRepository;
-    private final BusinessEventNotifierService businessEventNotifierService;
 
     @Override
     @Transactional
@@ -141,7 +143,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
         if (effectiveTransfers.size() == 2) {
             throw new ExternalAssetOwnerInitiateTransferException("This loan 
cannot be sold, there is already an in progress transfer");
         } else if (effectiveTransfers.size() == 1) {
-            if 
(ExternalTransferStatus.PENDING.equals(effectiveTransfers.get(0).getStatus())) {
+            if (PENDING.equals(effectiveTransfers.get(0).getStatus())) {
                 throw new ExternalAssetOwnerInitiateTransferException(
                         "External asset owner transfer is already in PENDING 
state for this loan");
             } else if 
(ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.get(0).getStatus())) {
@@ -190,8 +192,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
         } else if (!Objects.equals(effective.get(0).getId(), 
selectedTransfer.getId())) {
             throw new ExternalAssetOwnerInitiateTransferException(
                     String.format("This loan cannot be cancelled, selected 
transfer is not the latest"));
-        } else if (selectedTransfer.getStatus() != 
ExternalTransferStatus.PENDING
-                && selectedTransfer.getStatus() != 
ExternalTransferStatus.BUYBACK) {
+        } else if (selectedTransfer.getStatus() != PENDING && 
selectedTransfer.getStatus() != ExternalTransferStatus.BUYBACK) {
             throw new ExternalAssetOwnerInitiateTransferException(
                     "This loan cannot be cancelled, the selected transfer 
status is not pending or buyback");
         }
@@ -256,7 +257,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
     }
 
     private void validateLoanStatus(LoanIdAndExternalIdAndStatus entity) {
-        if 
(!NON_CLOSED_LOAN_STATUSES.contains(LoanStatus.fromInt(entity.getLoanStatus())))
 {
+        if 
(!ACTIVE_LOAN_STATUSES.contains(LoanStatus.fromInt(entity.getLoanStatus()))) {
             throw new ExternalAssetOwnerInitiateTransferException("Loan is not 
in active status");
         }
     }
@@ -268,7 +269,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements 
ExternalAssetOwnersW
         ExternalAssetOwner owner = getOwner(json);
         externalAssetOwnerTransfer.setOwner(owner);
         
externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json));
-        externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING);
+        externalAssetOwnerTransfer.setStatus(PENDING);
         
externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json));
         
externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json));
         externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveFrom);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferService.java
similarity index 69%
copy from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java
copy to 
fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferService.java
index 0dc2718c2..d51fb819b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferService.java
@@ -16,20 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.event.business.domain.loan;
+package org.apache.fineract.investor.service;
 
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 
-public class LoanStatusChangedBusinessEvent extends LoanBusinessEvent {
+public interface LoanAccountOwnerTransferService {
 
-    private static final String TYPE = "LoanStatusChangedBusinessEvent";
-
-    public LoanStatusChangedBusinessEvent(Loan value) {
-        super(value);
-    }
-
-    @Override
-    public String getType() {
-        return TYPE;
-    }
+    void handleLoanClosedOrOverpaid(Loan loan);
 }
diff --git 
a/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java
 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java
new file mode 100644
index 000000000..b87625126
--- /dev/null
+++ 
b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceImpl.java
@@ -0,0 +1,208 @@
+/**
+ * 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.service;
+
+import static 
org.apache.fineract.infrastructure.core.service.DateUtils.getBusinessLocalDate;
+import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
+import static 
org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED;
+import static 
org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED;
+import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
+import static 
org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_NEGATIVE;
+import static 
org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_ZERO;
+import static 
org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Objects;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.interoperation.util.MathUtil;
+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.ExternalAssetOwnerTransferDetails;
+import 
org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
+import 
org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
+import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@RequiredArgsConstructor
+@Transactional
+@Slf4j
+public class LoanAccountOwnerTransferServiceImpl implements 
LoanAccountOwnerTransferService {
+
+    public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 
12, 31);
+    private final ExternalAssetOwnerTransferRepository 
externalAssetOwnerTransferRepository;
+    private final ExternalAssetOwnerTransferLoanMappingRepository 
externalAssetOwnerTransferLoanMappingRepository;
+    private final AccountingService accountingService;
+    private final BusinessEventNotifierService businessEventNotifierService;
+
+    @Override
+    public void handleLoanClosedOrOverpaid(Loan loan) {
+        Long loanId = loan.getId();
+        List<ExternalAssetOwnerTransfer> transferDataList = 
findAllPendingOrBuybackTransfers(loanId);
+
+        if (transferDataList.size() == 2) {
+            ExternalTransferSubStatus subStatus;
+            if (isSameDayTransfers(transferDataList)) {
+                subStatus = SAMEDAY_TRANSFERS;
+            } else {
+                subStatus = isBiggerThanZero(loan.getTotalOverpaid()) ? 
BALANCE_NEGATIVE : BALANCE_ZERO;
+            }
+            cancelPendingSaleAndBuybackTransfer(loan, transferDataList, 
subStatus);
+        } else if (transferDataList.size() == 1) {
+            ExternalAssetOwnerTransfer transfer = transferDataList.get(0);
+            if (PENDING.equals(transfer.getStatus())) {
+                declinePendingSaleTransfer(loan, transfer);
+            } else if (BUYBACK.equals(transfer.getStatus())) {
+                executePendingBuybackTransfer(loan, transfer);
+            }
+        }
+    }
+
+    private void cancelPendingSaleAndBuybackTransfer(Loan loan, 
List<ExternalAssetOwnerTransfer> transferDataList,
+            ExternalTransferSubStatus subStatus) {
+        ExternalAssetOwnerTransfer pendingSaleTransfer = 
transferDataList.get(0);
+        updatePendingTransfer(pendingSaleTransfer);
+        ExternalAssetOwnerTransfer cancelledSaleTransfer = 
createCancelledTransfer(pendingSaleTransfer, subStatus);
+
+        ExternalAssetOwnerTransfer pendingBuybackTransfer = 
transferDataList.get(1);
+        updatePendingTransfer(pendingBuybackTransfer);
+        ExternalAssetOwnerTransfer cancelledBuybackTransfer = 
createCancelledTransfer(pendingBuybackTransfer, subStatus);
+
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanOwnershipTransferBusinessEvent(cancelledSaleTransfer, loan));
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanOwnershipTransferBusinessEvent(cancelledBuybackTransfer, loan));
+    }
+
+    private void declinePendingSaleTransfer(Loan loan, 
ExternalAssetOwnerTransfer pendingSaleTransfer) {
+        ExternalAssetOwnerTransfer declinedSaleTransfer = 
createDeclinedTransfer(pendingSaleTransfer, loan);
+        updatePendingTransfer(pendingSaleTransfer);
+
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanOwnershipTransferBusinessEvent(declinedSaleTransfer, loan));
+    }
+
+    private void executePendingBuybackTransfer(final Loan loan, 
ExternalAssetOwnerTransfer buybackTransfer) {
+        ExternalAssetOwnerTransfer activeTransfer = findActiveTransfer(loan, 
buybackTransfer);
+        updateActiveTransfer(activeTransfer);
+        buybackTransfer = updatePendingBuybackTransfer(loan, buybackTransfer);
+
+        
externalAssetOwnerTransferLoanMappingRepository.deleteByLoanIdAndOwnerTransfer(loan.getId(),
 activeTransfer);
+        accountingService.createJournalEntriesForBuybackAssetTransfer(loan, 
buybackTransfer);
+
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanOwnershipTransferBusinessEvent(buybackTransfer, loan));
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanAccountSnapshotBusinessEvent(loan));
+    }
+
+    private ExternalAssetOwnerTransfer 
createCancelledTransfer(ExternalAssetOwnerTransfer pendingTransfer,
+            ExternalTransferSubStatus subStatus) {
+        ExternalAssetOwnerTransfer cancelledTransfer = new 
ExternalAssetOwnerTransfer();
+        cancelledTransfer.setOwner(pendingTransfer.getOwner());
+        cancelledTransfer.setExternalId(pendingTransfer.getExternalId());
+        cancelledTransfer.setStatus(CANCELLED);
+        cancelledTransfer.setSubStatus(subStatus);
+        
cancelledTransfer.setSettlementDate(pendingTransfer.getSettlementDate());
+        cancelledTransfer.setLoanId(pendingTransfer.getLoanId());
+        
cancelledTransfer.setExternalLoanId(pendingTransfer.getExternalLoanId());
+        
cancelledTransfer.setPurchasePriceRatio(pendingTransfer.getPurchasePriceRatio());
+        cancelledTransfer.setEffectiveDateFrom(getBusinessLocalDate());
+        cancelledTransfer.setEffectiveDateTo(getBusinessLocalDate());
+        return externalAssetOwnerTransferRepository.save(cancelledTransfer);
+    }
+
+    private ExternalAssetOwnerTransfer 
createDeclinedTransfer(ExternalAssetOwnerTransfer pendingSaleTransfer, Loan 
loan) {
+        ExternalAssetOwnerTransfer declinedTransfer = new 
ExternalAssetOwnerTransfer();
+        declinedTransfer.setOwner(pendingSaleTransfer.getOwner());
+        declinedTransfer.setExternalId(pendingSaleTransfer.getExternalId());
+        declinedTransfer.setStatus(DECLINED);
+        
declinedTransfer.setSubStatus(isBiggerThanZero(loan.getTotalOverpaid()) ? 
BALANCE_NEGATIVE : BALANCE_ZERO);
+        
declinedTransfer.setSettlementDate(pendingSaleTransfer.getSettlementDate());
+        declinedTransfer.setLoanId(pendingSaleTransfer.getLoanId());
+        
declinedTransfer.setExternalLoanId(pendingSaleTransfer.getExternalLoanId());
+        
declinedTransfer.setPurchasePriceRatio(pendingSaleTransfer.getPurchasePriceRatio());
+        declinedTransfer.setEffectiveDateFrom(getBusinessLocalDate());
+        declinedTransfer.setEffectiveDateTo(getBusinessLocalDate());
+        return externalAssetOwnerTransferRepository.save(declinedTransfer);
+    }
+
+    private void updatePendingTransfer(ExternalAssetOwnerTransfer 
pendingTransfer) {
+        pendingTransfer.setEffectiveDateTo(getBusinessLocalDate());
+        externalAssetOwnerTransferRepository.save(pendingTransfer);
+    }
+
+    private ExternalAssetOwnerTransfer updatePendingBuybackTransfer(Loan loan, 
ExternalAssetOwnerTransfer buybackTransfer) {
+        buybackTransfer.setEffectiveDateTo(getBusinessLocalDate());
+        
buybackTransfer.setExternalAssetOwnerTransferDetails(createAssetOwnerTransferDetails(loan,
 buybackTransfer));
+        return externalAssetOwnerTransferRepository.save(buybackTransfer);
+    }
+
+    private void updateActiveTransfer(ExternalAssetOwnerTransfer 
activeTransfer) {
+        activeTransfer.setEffectiveDateTo(getBusinessLocalDate());
+        externalAssetOwnerTransferRepository.save(activeTransfer);
+    }
+
+    private ExternalAssetOwnerTransferDetails 
createAssetOwnerTransferDetails(Loan loan,
+            ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
+        ExternalAssetOwnerTransferDetails details = new 
ExternalAssetOwnerTransferDetails();
+        details.setExternalAssetOwnerTransfer(externalAssetOwnerTransfer);
+        
details.setTotalOutstanding(Objects.requireNonNullElse(loan.getLoanSummary().getTotalOutstanding(),
 BigDecimal.ZERO));
+        details.setTotalPrincipalOutstanding(
+                
Objects.requireNonNullElse(loan.getLoanSummary().getTotalPrincipalOutstanding(),
 BigDecimal.ZERO));
+        details.setTotalInterestOutstanding(
+                
Objects.requireNonNullElse(loan.getLoanSummary().getTotalInterestOutstanding(), 
BigDecimal.ZERO));
+        details.setTotalFeeChargesOutstanding(
+                
Objects.requireNonNullElse(loan.getLoanSummary().getTotalFeeChargesOutstanding(),
 BigDecimal.ZERO));
+        details.setTotalPenaltyChargesOutstanding(
+                
Objects.requireNonNullElse(loan.getLoanSummary().getTotalPenaltyChargesOutstanding(),
 BigDecimal.ZERO));
+        
details.setTotalOverpaid(Objects.requireNonNullElse(loan.getTotalOverpaid(), 
BigDecimal.ZERO));
+        return details;
+    }
+
+    private ExternalAssetOwnerTransfer findActiveTransfer(Loan loan, 
ExternalAssetOwnerTransfer buybackTransfer) {
+        return externalAssetOwnerTransferRepository
+                .findOne((root, query, criteriaBuilder) -> 
criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loan.getId()),
+                        criteriaBuilder.equal(root.get("owner"), 
buybackTransfer.getOwner()),
+                        criteriaBuilder.equal(root.get("status"), 
ExternalTransferStatus.ACTIVE),
+                        criteriaBuilder.equal(root.get("effectiveDateTo"), 
FUTURE_DATE_9999_12_31)))
+                .orElseThrow();
+    }
+
+    private List<ExternalAssetOwnerTransfer> 
findAllPendingOrBuybackTransfers(Long loanId) {
+        return externalAssetOwnerTransferRepository
+                .findAll(
+                        (root, query, criteriaBuilder) -> 
criteriaBuilder.and(criteriaBuilder.equal(root.get("loanId"), loanId),
+                                root.get("status").in(List.of(PENDING, 
BUYBACK)),
+                                
criteriaBuilder.equal(root.get("effectiveDateTo"), FUTURE_DATE_9999_12_31)),
+                        Sort.by(Sort.Direction.ASC, "id"));
+    }
+
+    private boolean isBiggerThanZero(BigDecimal loanTotalOverpaid) {
+        return MathUtil.nullToDefault(loanTotalOverpaid, 
BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0;
+    }
+
+    private static boolean isSameDayTransfers(List<ExternalAssetOwnerTransfer> 
transferDataList) {
+        return Objects.equals(transferDataList.get(0).getSettlementDate(), 
transferDataList.get(1).getSettlementDate());
+    }
+}
diff --git 
a/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java
 
b/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java
new file mode 100644
index 000000000..adf33d889
--- /dev/null
+++ 
b/fineract-investor/src/test/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferServiceTest.java
@@ -0,0 +1,179 @@
+/**
+ * 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.service;
+
+import static 
org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
+import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
+import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+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 static org.springframework.data.domain.Sort.Direction.ASC;
+
+import java.time.LocalDate;
+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.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+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.domain.LoanOwnershipTransferBusinessEvent;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
+import org.jetbrains.annotations.NotNull;
+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 LoanAccountOwnerTransferServiceTest {
+
+    @Mock
+    private ExternalAssetOwnerTransferRepository 
externalAssetOwnerTransferRepository;
+    @Mock
+    private ExternalAssetOwnerTransferLoanMappingRepository 
externalAssetOwnerTransferLoanMappingRepository;
+    @Mock
+    private AccountingService accountingService;
+    @Mock
+    private BusinessEventNotifierService businessEventNotifierService;
+
+    private LoanAccountOwnerTransferService underTest;
+    private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
+
+    @BeforeEach
+    public void setUp() {
+        ThreadLocalContextUtil.setBusinessDates(new 
HashMap<>(Map.of(BUSINESS_DATE, actualDate)));
+        underTest = new 
LoanAccountOwnerTransferServiceImpl(externalAssetOwnerTransferRepository,
+                externalAssetOwnerTransferLoanMappingRepository, 
accountingService, businessEventNotifierService);
+    }
+
+    @Test
+    public void 
verifyWhenCancelPendingSaleAndBuybackTransferThenBusinessEventsAreSent() {
+        // given
+        final Loan loanForProcessing = Mockito.mock(Loan.class);
+        when(loanForProcessing.getId()).thenReturn(1L);
+
+        ExternalAssetOwnerTransfer pendingSaleTransfer = 
Mockito.mock(ExternalAssetOwnerTransfer.class);
+        ExternalAssetOwnerTransfer pendingBuybackTransfer = 
Mockito.mock(ExternalAssetOwnerTransfer.class);
+        ExternalAssetOwnerTransfer cancelledSaleTransfer = 
Mockito.mock(ExternalAssetOwnerTransfer.class);
+        ExternalAssetOwnerTransfer cancelledBuybackTransfer = 
Mockito.mock(ExternalAssetOwnerTransfer.class);
+
+        List<ExternalAssetOwnerTransfer> response = 
List.of(pendingSaleTransfer, pendingBuybackTransfer);
+        
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), 
eq(Sort.by(ASC, "id")))).thenReturn(response);
+        
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(pendingSaleTransfer)
+                
.thenReturn(cancelledSaleTransfer).thenReturn(pendingBuybackTransfer).thenReturn(cancelledBuybackTransfer);
+
+        // when
+        underTest.handleLoanClosedOrOverpaid(loanForProcessing);
+
+        // then
+        ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = 
verifyBusinessEvents(2);
+        verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, 
loanForProcessing, cancelledSaleTransfer);
+        verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, 
loanForProcessing, cancelledBuybackTransfer);
+    }
+
+    @Test
+    public void verifyWhenDeclinePendingSaleTransferThenBusinessEventIsSent() {
+        // given
+        final Loan loanForProcessing = Mockito.mock(Loan.class);
+        when(loanForProcessing.getId()).thenReturn(1L);
+
+        ExternalAssetOwnerTransfer pendingSaleTransfer = 
Mockito.mock(ExternalAssetOwnerTransfer.class);
+        when(pendingSaleTransfer.getStatus()).thenReturn(PENDING);
+        ExternalAssetOwnerTransfer declineTransfer = 
Mockito.mock(ExternalAssetOwnerTransfer.class);
+        List<ExternalAssetOwnerTransfer> response = 
List.of(pendingSaleTransfer);
+
+        
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), 
eq(Sort.by(ASC, "id")))).thenReturn(response);
+        
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(declineTransfer)
+                .thenReturn(pendingSaleTransfer);
+
+        // when
+        underTest.handleLoanClosedOrOverpaid(loanForProcessing);
+
+        // then
+        ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = 
verifyBusinessEvents(1);
+        verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, 
loanForProcessing, declineTransfer);
+    }
+
+    @Test
+    public void 
verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent() {
+        // given
+        final Loan loanForProcessing = Mockito.mock(Loan.class);
+        when(loanForProcessing.getId()).thenReturn(1L);
+        LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
+        when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary);
+
+        ExternalAssetOwnerTransfer pendingBuybackTransfer = 
Mockito.mock(ExternalAssetOwnerTransfer.class);
+        when(pendingBuybackTransfer.getStatus()).thenReturn(BUYBACK);
+        ExternalAssetOwnerTransfer activeTransfer = 
Mockito.mock(ExternalAssetOwnerTransfer.class);
+        List<ExternalAssetOwnerTransfer> response = 
List.of(pendingBuybackTransfer);
+
+        
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), 
eq(Sort.by(ASC, "id")))).thenReturn(response);
+        
when(externalAssetOwnerTransferRepository.findOne(any(Specification.class))).thenReturn(Optional.of(activeTransfer));
+        
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(activeTransfer)
+                .thenReturn(pendingBuybackTransfer);
+
+        // when
+        underTest.handleLoanClosedOrOverpaid(loanForProcessing);
+
+        // then
+        ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = 
verifyBusinessEvents(2);
+        verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, 
loanForProcessing, pendingBuybackTransfer);
+        verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, 
loanForProcessing);
+    }
+
+    @NotNull
+    private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int 
expectedBusinessEvents) {
+        @SuppressWarnings("unchecked")
+        ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = 
ArgumentCaptor.forClass(BusinessEvent.class);
+        verify(businessEventNotifierService, 
times(expectedBusinessEvents)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture());
+        return businessEventArgumentCaptor;
+    }
+
+    private void 
verifyLoanTransferBusinessEvent(ArgumentCaptor<BusinessEvent<?>> 
businessEventArgumentCaptor, int index, Loan expectedLoan,
+            ExternalAssetOwnerTransfer expectedAssetOwnerTransfer) {
+        assertTrue(businessEventArgumentCaptor.getAllValues().get(index) 
instanceof LoanOwnershipTransferBusinessEvent);
+        assertEquals(expectedLoan, ((LoanOwnershipTransferBusinessEvent) 
businessEventArgumentCaptor.getAllValues().get(index)).getLoan());
+        assertEquals(expectedAssetOwnerTransfer,
+                ((LoanOwnershipTransferBusinessEvent) 
businessEventArgumentCaptor.getAllValues().get(index)).get());
+    }
+
+    private void 
verifyLoanAccountSnapshotBusinessEvent(ArgumentCaptor<BusinessEvent<?>> 
businessEventArgumentCaptor, int index,
+            Loan expectedLoan) {
+        assertTrue(businessEventArgumentCaptor.getAllValues().get(index) 
instanceof LoanAccountSnapshotBusinessEvent);
+        assertEquals(expectedLoan, ((LoanAccountSnapshotBusinessEvent) 
businessEventArgumentCaptor.getAllValues().get(index)).get());
+    }
+
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java
 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java
similarity index 100%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java
rename to 
fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanStatusChangedBusinessEvent.java
diff --git 
a/fineract-loan/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
index fce514133..07263eeb7 100644
--- 
a/fineract-loan/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
@@ -81,7 +81,6 @@ public interface LoanRepository extends JpaRepository<Loan, 
Long>, JpaSpecificat
     String FIND_BY_ACCOUNT_NUMBER = "select loan from Loan loan where 
loan.accountNumber = :accountNumber";
 
     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)";
 
     String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE 
loan.externalId = :externalId";
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index f2aaa99e3..0578e15a4 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -135,4 +135,5 @@
     <include 
file="parts/0113_transaction_summary_with_asset_owner_report_sql_fix.xml" 
relativeToChangelogFile="true" />
     <include file="parts/0114_create_cob_indices.xml" 
relativeToChangelogFile="true" />
     <include file="parts/0115_create_index_from_loan_transaction_id.xml" 
relativeToChangelogFile="true" />
+    <include 
file="parts/0116_add_configuration_asset_externalization_of_non_active_loans.xml"
 relativeToChangelogFile="true" />
 </databaseChangeLog>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0116_add_configuration_asset_externalization_of_non_active_loans.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0116_add_configuration_asset_externalization_of_non_active_loans.xml
new file mode 100644
index 000000000..103571317
--- /dev/null
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0116_add_configuration_asset_externalization_of_non_active_loans.xml
@@ -0,0 +1,41 @@
+<?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.3.xsd";>
+    <changeSet author="fineract" id="1" context="postgresql">
+        <sql>
+            SELECT SETVAL('c_configuration_id_seq', COALESCE(MAX(id), 0)+1, 
false ) FROM c_configuration;
+        </sql>
+    </changeSet>
+    <changeSet author="fineract" id="2">
+        <insert tableName="c_configuration">
+            <column name="name" 
value="asset-externalization-of-non-active-loans"/>
+            <column name="value"/>
+            <column name="date_value"/>
+            <column name="string_value"/>
+            <column name="enabled" valueBoolean="true"/>
+            <column name="is_trap_door" valueBoolean="false"/>
+            <column name="description" value="If enabled: when a loan state is 
changed to non-active -> pending transfers will be handled"/>
+        </insert>
+    </changeSet>
+</databaseChangeLog>
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
index 6a99e5683..4fe958727 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
@@ -119,9 +119,8 @@ public class GlobalConfigurationHelper {
         ArrayList<HashMap> expectedGlobalConfigurations = 
getAllDefaultGlobalConfigurations();
         ArrayList<HashMap> actualGlobalConfigurations = 
getAllGlobalConfigurations(requestSpec, responseSpec);
 
-        // There are currently 50 global configurations.
-        Assertions.assertEquals(51, expectedGlobalConfigurations.size());
-        Assertions.assertEquals(51, actualGlobalConfigurations.size());
+        Assertions.assertEquals(52, expectedGlobalConfigurations.size());
+        Assertions.assertEquals(52, actualGlobalConfigurations.size());
 
         for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
 
@@ -566,6 +565,15 @@ public class GlobalConfigurationHelper {
         accrualForChargeDate.put("string_value", "due-date");
         defaults.add(accrualForChargeDate);
 
+        HashMap<String, Object> assetExternalizationOfNonActiveLoans = new 
HashMap<>();
+        assetExternalizationOfNonActiveLoans.put("id", 57);
+        assetExternalizationOfNonActiveLoans.put("name", 
"asset-externalization-of-non-active-loans");
+        assetExternalizationOfNonActiveLoans.put("value", 0);
+        assetExternalizationOfNonActiveLoans.put("enabled", true);
+        assetExternalizationOfNonActiveLoans.put("trapDoor", false);
+        assetExternalizationOfNonActiveLoans.put("string_value", "due-date");
+        defaults.add(assetExternalizationOfNonActiveLoans);
+
         return defaults;
     }
 
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 ca520088e..bb745e5bb 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
@@ -21,7 +21,10 @@ package 
org.apache.fineract.integrationtests.investor.externalassetowner;
 import static 
org.apache.fineract.client.models.ExternalTransferData.StatusEnum.ACTIVE;
 import static 
org.apache.fineract.client.models.ExternalTransferData.StatusEnum.BUYBACK;
 import static 
org.apache.fineract.client.models.ExternalTransferData.StatusEnum.CANCELLED;
+import static 
org.apache.fineract.client.models.ExternalTransferData.StatusEnum.DECLINED;
 import static 
org.apache.fineract.client.models.ExternalTransferData.StatusEnum.PENDING;
+import static 
org.apache.fineract.client.models.ExternalTransferData.SubStatusEnum.BALANCE_ZERO;
+import static 
org.apache.fineract.client.models.ExternalTransferData.SubStatusEnum.SAMEDAY_TRANSFERS;
 import static 
org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -496,6 +499,118 @@ public class InitiateExternalAssetOwnerTransferTest {
         }
     }
 
+    @Test
+    public void saleIsDeclinedWhenLoanIsCancelled() {
+        try {
+            GlobalConfigurationHelper.manageConfigurations(REQUEST_SPEC, 
RESPONSE_SPEC,
+                    
GlobalConfigurationHelper.ENABLE_AUTOGENERATED_EXTERNAL_ID, true);
+            setInitialBusinessDate("2020-03-02");
+            Integer clientID = createClient();
+            Integer loanID = createLoanForClient(clientID);
+
+            PostInitiateTransferResponse saleTransferResponse = 
createSaleTransfer(loanID, "2020-03-06");
+            updateBusinessDateAndExecuteCOBJob("2020-03-04");
+
+            LOAN_TRANSACTION_HELPER.writeOffLoan("04 March 2020", loanID);
+
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, 
saleTransferResponse.getResourceExternalId(), "2020-03-06", "2020-03-02",
+                            "2020-03-04"),
+                    ExpectedExternalTransferData.expected(DECLINED, 
saleTransferResponse.getResourceExternalId(), "2020-03-06",
+                            "2020-03-04", "2020-03-04", BALANCE_ZERO));
+        } finally {
+            cleanUpAndRestoreBusinessDate();
+        }
+    }
+
+    @Test
+    public void buybackIsExecutedWhenLoanIsCancelled() {
+        try {
+            GlobalConfigurationHelper.manageConfigurations(REQUEST_SPEC, 
RESPONSE_SPEC,
+                    
GlobalConfigurationHelper.ENABLE_AUTOGENERATED_EXTERNAL_ID, true);
+            setInitialBusinessDate("2020-03-02");
+            Integer clientID = createClient();
+            Integer loanID = createLoanForClient(clientID);
+
+            PostInitiateTransferResponse saleTransferResponse = 
createSaleTransfer(loanID, "2020-03-04");
+            updateBusinessDateAndExecuteCOBJob("2020-03-05");
+            PostInitiateTransferResponse buybackTransferResponse = 
createBuybackTransfer(loanID, "2020-03-06");
+
+            LOAN_TRANSACTION_HELPER.writeOffLoan("04 March 2020", loanID);
+
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, 
saleTransferResponse.getResourceExternalId(), "2020-03-04", "2020-03-02",
+                            "2020-03-04"),
+                    ExpectedExternalTransferData.expected(ACTIVE, 
saleTransferResponse.getResourceExternalId(), "2020-03-04", "2020-03-05",
+                            "2020-03-05", true, new 
BigDecimal("15757.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new 
BigDecimal("0.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")),
+                    ExpectedExternalTransferData.expected(BUYBACK, 
buybackTransferResponse.getResourceExternalId(), "2020-03-06",
+                            "2020-03-05", "2020-03-05", true, new 
BigDecimal("15757.420000"), new BigDecimal("15000.000000"),
+                            new BigDecimal("757.420000"), new 
BigDecimal("0.000000"), new BigDecimal("0.000000"),
+                            new BigDecimal("0.000000")));
+            
getAndValidateThereIsNoActiveMapping(saleTransferResponse.getResourceExternalId());
+        } finally {
+            cleanUpAndRestoreBusinessDate();
+        }
+    }
+
+    @Test
+    public void buybackAndSaleIsCancelledWhenLoanIsCancelled() {
+        try {
+            GlobalConfigurationHelper.manageConfigurations(REQUEST_SPEC, 
RESPONSE_SPEC,
+                    
GlobalConfigurationHelper.ENABLE_AUTOGENERATED_EXTERNAL_ID, true);
+            setInitialBusinessDate("2020-03-02");
+            Integer clientID = createClient();
+            Integer loanID = createLoanForClient(clientID);
+
+            PostInitiateTransferResponse saleTransferResponse = 
createSaleTransfer(loanID, "2020-03-04");
+            PostInitiateTransferResponse buybackTransferResponse = 
createBuybackTransfer(loanID, "2020-03-06");
+
+            LOAN_TRANSACTION_HELPER.writeOffLoan("02 March 2020", loanID);
+
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, 
saleTransferResponse.getResourceExternalId(), "2020-03-04", "2020-03-02",
+                            "2020-03-02"),
+                    ExpectedExternalTransferData.expected(BUYBACK, 
buybackTransferResponse.getResourceExternalId(), "2020-03-06",
+                            "2020-03-02", "2020-03-02"),
+                    ExpectedExternalTransferData.expected(CANCELLED, 
buybackTransferResponse.getResourceExternalId(), "2020-03-06",
+                            "2020-03-02", "2020-03-02", BALANCE_ZERO),
+                    ExpectedExternalTransferData.expected(CANCELLED, 
saleTransferResponse.getResourceExternalId(), "2020-03-04",
+                            "2020-03-02", "2020-03-02", BALANCE_ZERO));
+        } finally {
+            cleanUpAndRestoreBusinessDate();
+        }
+    }
+
+    @Test
+    public void sameDayBuybackAndSaleIsCancelledWhenLoanIsCancelled() {
+        try {
+            GlobalConfigurationHelper.manageConfigurations(REQUEST_SPEC, 
RESPONSE_SPEC,
+                    
GlobalConfigurationHelper.ENABLE_AUTOGENERATED_EXTERNAL_ID, true);
+            setInitialBusinessDate("2020-03-02");
+            Integer clientID = createClient();
+            Integer loanID = createLoanForClient(clientID);
+
+            PostInitiateTransferResponse saleTransferResponse = 
createSaleTransfer(loanID, "2020-03-03");
+            PostInitiateTransferResponse buybackTransferResponse = 
createBuybackTransfer(loanID, "2020-03-03");
+
+            LOAN_TRANSACTION_HELPER.writeOffLoan("02 March 2020", loanID);
+
+            getAndValidateExternalAssetOwnerTransferByLoan(loanID,
+                    ExpectedExternalTransferData.expected(PENDING, 
saleTransferResponse.getResourceExternalId(), "2020-03-03", "2020-03-02",
+                            "2020-03-02"),
+                    ExpectedExternalTransferData.expected(BUYBACK, 
buybackTransferResponse.getResourceExternalId(), "2020-03-03",
+                            "2020-03-02", "2020-03-02"),
+                    ExpectedExternalTransferData.expected(CANCELLED, 
buybackTransferResponse.getResourceExternalId(), "2020-03-03",
+                            "2020-03-02", "2020-03-02", SAMEDAY_TRANSFERS),
+                    ExpectedExternalTransferData.expected(CANCELLED, 
saleTransferResponse.getResourceExternalId(), "2020-03-03",
+                            "2020-03-02", "2020-03-02", SAMEDAY_TRANSFERS));
+        } finally {
+            cleanUpAndRestoreBusinessDate();
+        }
+    }
+
     @Test
     public void saleAndBuybackOnTheSameDay() {
         try {
@@ -592,8 +707,11 @@ public class InitiateExternalAssetOwnerTransferTest {
             CallFailedRuntimeException exception = 
assertThrows(CallFailedRuntimeException.class, () -> createBuybackTransfer(1, 
null));
             assertTrue(exception.getMessage().contains("The parameter 
`settlementDate` is mandatory."));
 
-            CallFailedRuntimeException exception2 = 
assertThrows(CallFailedRuntimeException.class,
-                    () -> createBuybackTransfer(1, "1970-01-01"));
+            CallFailedRuntimeException exception2 = 
assertThrows(CallFailedRuntimeException.class, () -> {
+                Integer clientID = createClient();
+                Integer loanID = createLoanForClient(clientID);
+                createBuybackTransfer(loanID, "1970-01-01");
+            });
             assertTrue(exception2.getMessage().contains("Settlement date 
cannot be in the past"));
 
             CallFailedRuntimeException exception3 = 
assertThrows(CallFailedRuntimeException.class, () -> {
@@ -839,6 +957,9 @@ public class InitiateExternalAssetOwnerTransferTest {
                 assertEquals(expected.totalFeeOutstanding, 
etd.getDetails().getTotalFeeChargesOutstanding());
                 assertEquals(expected.totalOverpaid, 
etd.getDetails().getTotalOverpaid());
             }
+            if (expected.subStatus != null) {
+                assertEquals(expected.subStatus, etd.getSubStatus());
+            }
         }
     }
 
@@ -919,6 +1040,7 @@ public class InitiateExternalAssetOwnerTransferTest {
 
         private final String effectiveFrom;
         private final String effectiveTo;
+        private final ExternalTransferData.SubStatusEnum subStatus;
         private final boolean detailsExpected;
         private final BigDecimal totalOutstanding;
         private final BigDecimal totalPrincipalOutstanding;
@@ -931,11 +1053,22 @@ public class InitiateExternalAssetOwnerTransferTest {
                 String settlementDate, String effectiveFrom, String 
effectiveTo, boolean detailsExpected, BigDecimal totalOutstanding,
                 BigDecimal totalPrincipalOutstanding, BigDecimal 
totalInterestOutstanding, BigDecimal totalPenaltyOutstanding,
                 BigDecimal totalFeeOutstanding, BigDecimal totalOverpaid) {
-            return new ExpectedExternalTransferData(status, 
transferExternalId, settlementDate, effectiveFrom, effectiveTo, detailsExpected,
-                    totalOutstanding, totalPrincipalOutstanding, 
totalInterestOutstanding, totalPenaltyOutstanding, totalFeeOutstanding,
-                    totalOverpaid);
+            return new ExpectedExternalTransferData(status, 
transferExternalId, settlementDate, effectiveFrom, effectiveTo, null,
+                    detailsExpected, totalOutstanding, 
totalPrincipalOutstanding, totalInterestOutstanding, totalPenaltyOutstanding,
+                    totalFeeOutstanding, totalOverpaid);
         }
 
+        static ExpectedExternalTransferData 
expected(ExternalTransferData.StatusEnum status, String transferExternalId,
+                String settlementDate, String effectiveFrom, String 
effectiveTo) {
+            return new ExpectedExternalTransferData(status, 
transferExternalId, settlementDate, effectiveFrom, effectiveTo, null, false,
+                    null, null, null, null, null, null);
+        }
+
+        static ExpectedExternalTransferData 
expected(ExternalTransferData.StatusEnum status, String transferExternalId,
+                String settlementDate, String effectiveFrom, String 
effectiveTo, ExternalTransferData.SubStatusEnum subStatus) {
+            return new ExpectedExternalTransferData(status, 
transferExternalId, settlementDate, effectiveFrom, effectiveTo, subStatus,
+                    false, null, null, null, null, null, null);
+        }
     }
 
     @RequiredArgsConstructor()

Reply via email to