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()