FINERACT-1724: Asset externalization of non-active loans
This commit is contained in:
committed by
Adam Saghy
parent
7c80bf2ffc
commit
61915bbcc4
+21
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
public interface ExternalAssetOwnerLoanStatusChangePlatformService {}
|
||||||
+57
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-9
@@ -18,6 +18,11 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.investor.service;
|
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.JsonElement;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import java.lang.reflect.Type;
|
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.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
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.ExternalTransferRequestParameters;
|
||||||
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
import org.apache.fineract.investor.data.ExternalTransferStatus;
|
||||||
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
|
||||||
@@ -65,15 +69,13 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersWriteService {
|
public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersWriteService {
|
||||||
|
|
||||||
private static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
|
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,
|
private static final List<LoanStatus> ACTIVE_LOAN_STATUSES = List.of(ACTIVE, TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD);
|
||||||
LoanStatus.ACTIVE, LoanStatus.TRANSFER_IN_PROGRESS, LoanStatus.TRANSFER_ON_HOLD);
|
|
||||||
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING,
|
private static final List<ExternalTransferStatus> BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING,
|
||||||
ExternalTransferStatus.ACTIVE);
|
ExternalTransferStatus.ACTIVE);
|
||||||
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
private final ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
|
||||||
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
|
private final ExternalAssetOwnerRepository externalAssetOwnerRepository;
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
private final LoanRepository loanRepository;
|
private final LoanRepository loanRepository;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -141,7 +143,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
if (effectiveTransfers.size() == 2) {
|
if (effectiveTransfers.size() == 2) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
throw new ExternalAssetOwnerInitiateTransferException("This loan cannot be sold, there is already an in progress transfer");
|
||||||
} else if (effectiveTransfers.size() == 1) {
|
} else if (effectiveTransfers.size() == 1) {
|
||||||
if (ExternalTransferStatus.PENDING.equals(effectiveTransfers.get(0).getStatus())) {
|
if (PENDING.equals(effectiveTransfers.get(0).getStatus())) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"External asset owner transfer is already in PENDING state for this loan");
|
"External asset owner transfer is already in PENDING state for this loan");
|
||||||
} else if (ExternalTransferStatus.ACTIVE.equals(effectiveTransfers.get(0).getStatus())) {
|
} 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())) {
|
} else if (!Objects.equals(effective.get(0).getId(), selectedTransfer.getId())) {
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
String.format("This loan cannot be cancelled, selected transfer is not the latest"));
|
String.format("This loan cannot be cancelled, selected transfer is not the latest"));
|
||||||
} else if (selectedTransfer.getStatus() != ExternalTransferStatus.PENDING
|
} else if (selectedTransfer.getStatus() != PENDING && selectedTransfer.getStatus() != ExternalTransferStatus.BUYBACK) {
|
||||||
&& selectedTransfer.getStatus() != ExternalTransferStatus.BUYBACK) {
|
|
||||||
throw new ExternalAssetOwnerInitiateTransferException(
|
throw new ExternalAssetOwnerInitiateTransferException(
|
||||||
"This loan cannot be cancelled, the selected transfer status is not pending or buyback");
|
"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) {
|
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");
|
throw new ExternalAssetOwnerInitiateTransferException("Loan is not in active status");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,7 +269,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW
|
|||||||
ExternalAssetOwner owner = getOwner(json);
|
ExternalAssetOwner owner = getOwner(json);
|
||||||
externalAssetOwnerTransfer.setOwner(owner);
|
externalAssetOwnerTransfer.setOwner(owner);
|
||||||
externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json));
|
externalAssetOwnerTransfer.setExternalId(getTransferExternalIdFromJson(json));
|
||||||
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING);
|
externalAssetOwnerTransfer.setStatus(PENDING);
|
||||||
externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json));
|
externalAssetOwnerTransfer.setPurchasePriceRatio(getPurchasePriceRatioFromJson(json));
|
||||||
externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json));
|
externalAssetOwnerTransfer.setSettlementDate(getSettlementDateFromJson(json));
|
||||||
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveFrom);
|
externalAssetOwnerTransfer.setEffectiveDateFrom(effectiveFrom);
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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 org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
|
||||||
|
public interface LoanAccountOwnerTransferService {
|
||||||
|
|
||||||
|
void handleLoanClosedOrOverpaid(Loan loan);
|
||||||
|
}
|
||||||
+208
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+179
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
-1
@@ -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_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 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 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";
|
String FIND_ID_BY_EXTERNAL_ID = "SELECT loan.id FROM Loan loan WHERE loan.externalId = :externalId";
|
||||||
|
|||||||
@@ -135,4 +135,5 @@
|
|||||||
<include file="parts/0113_transaction_summary_with_asset_owner_report_sql_fix.xml" relativeToChangelogFile="true" />
|
<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/0114_create_cob_indices.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0115_create_index_from_loan_transaction_id.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>
|
</databaseChangeLog>
|
||||||
|
|||||||
+41
@@ -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>
|
||||||
+11
-3
@@ -119,9 +119,8 @@ public class GlobalConfigurationHelper {
|
|||||||
ArrayList<HashMap> expectedGlobalConfigurations = getAllDefaultGlobalConfigurations();
|
ArrayList<HashMap> expectedGlobalConfigurations = getAllDefaultGlobalConfigurations();
|
||||||
ArrayList<HashMap> actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec);
|
ArrayList<HashMap> actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec);
|
||||||
|
|
||||||
// There are currently 50 global configurations.
|
Assertions.assertEquals(52, expectedGlobalConfigurations.size());
|
||||||
Assertions.assertEquals(51, expectedGlobalConfigurations.size());
|
Assertions.assertEquals(52, actualGlobalConfigurations.size());
|
||||||
Assertions.assertEquals(51, actualGlobalConfigurations.size());
|
|
||||||
|
|
||||||
for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
|
for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
|
||||||
|
|
||||||
@@ -566,6 +565,15 @@ public class GlobalConfigurationHelper {
|
|||||||
accrualForChargeDate.put("string_value", "due-date");
|
accrualForChargeDate.put("string_value", "due-date");
|
||||||
defaults.add(accrualForChargeDate);
|
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;
|
return defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+138
-5
@@ -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.ACTIVE;
|
||||||
import static org.apache.fineract.client.models.ExternalTransferData.StatusEnum.BUYBACK;
|
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.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.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.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
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
|
@Test
|
||||||
public void saleAndBuybackOnTheSameDay() {
|
public void saleAndBuybackOnTheSameDay() {
|
||||||
try {
|
try {
|
||||||
@@ -592,8 +707,11 @@ public class InitiateExternalAssetOwnerTransferTest {
|
|||||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> createBuybackTransfer(1, null));
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> createBuybackTransfer(1, null));
|
||||||
assertTrue(exception.getMessage().contains("The parameter `settlementDate` is mandatory."));
|
assertTrue(exception.getMessage().contains("The parameter `settlementDate` is mandatory."));
|
||||||
|
|
||||||
CallFailedRuntimeException exception2 = assertThrows(CallFailedRuntimeException.class,
|
CallFailedRuntimeException exception2 = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||||
() -> createBuybackTransfer(1, "1970-01-01"));
|
Integer clientID = createClient();
|
||||||
|
Integer loanID = createLoanForClient(clientID);
|
||||||
|
createBuybackTransfer(loanID, "1970-01-01");
|
||||||
|
});
|
||||||
assertTrue(exception2.getMessage().contains("Settlement date cannot be in the past"));
|
assertTrue(exception2.getMessage().contains("Settlement date cannot be in the past"));
|
||||||
|
|
||||||
CallFailedRuntimeException exception3 = assertThrows(CallFailedRuntimeException.class, () -> {
|
CallFailedRuntimeException exception3 = assertThrows(CallFailedRuntimeException.class, () -> {
|
||||||
@@ -839,6 +957,9 @@ public class InitiateExternalAssetOwnerTransferTest {
|
|||||||
assertEquals(expected.totalFeeOutstanding, etd.getDetails().getTotalFeeChargesOutstanding());
|
assertEquals(expected.totalFeeOutstanding, etd.getDetails().getTotalFeeChargesOutstanding());
|
||||||
assertEquals(expected.totalOverpaid, etd.getDetails().getTotalOverpaid());
|
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 effectiveFrom;
|
||||||
private final String effectiveTo;
|
private final String effectiveTo;
|
||||||
|
private final ExternalTransferData.SubStatusEnum subStatus;
|
||||||
private final boolean detailsExpected;
|
private final boolean detailsExpected;
|
||||||
private final BigDecimal totalOutstanding;
|
private final BigDecimal totalOutstanding;
|
||||||
private final BigDecimal totalPrincipalOutstanding;
|
private final BigDecimal totalPrincipalOutstanding;
|
||||||
@@ -931,11 +1053,22 @@ public class InitiateExternalAssetOwnerTransferTest {
|
|||||||
String settlementDate, String effectiveFrom, String effectiveTo, boolean detailsExpected, BigDecimal totalOutstanding,
|
String settlementDate, String effectiveFrom, String effectiveTo, boolean detailsExpected, BigDecimal totalOutstanding,
|
||||||
BigDecimal totalPrincipalOutstanding, BigDecimal totalInterestOutstanding, BigDecimal totalPenaltyOutstanding,
|
BigDecimal totalPrincipalOutstanding, BigDecimal totalInterestOutstanding, BigDecimal totalPenaltyOutstanding,
|
||||||
BigDecimal totalFeeOutstanding, BigDecimal totalOverpaid) {
|
BigDecimal totalFeeOutstanding, BigDecimal totalOverpaid) {
|
||||||
return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, detailsExpected,
|
return new ExpectedExternalTransferData(status, transferExternalId, settlementDate, effectiveFrom, effectiveTo, null,
|
||||||
totalOutstanding, totalPrincipalOutstanding, totalInterestOutstanding, totalPenaltyOutstanding, totalFeeOutstanding,
|
detailsExpected, totalOutstanding, totalPrincipalOutstanding, totalInterestOutstanding, totalPenaltyOutstanding,
|
||||||
totalOverpaid);
|
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()
|
@RequiredArgsConstructor()
|
||||||
|
|||||||
Reference in New Issue
Block a user