FINERACT-1724: Asset externalization of non-active loans

This commit is contained in:
abraham.menyhart
2023-07-24 20:29:08 +02:00
committed by Adam Saghy
parent 7c80bf2ffc
commit 61915bbcc4
17 changed files with 692 additions and 18 deletions
@@ -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 {}
@@ -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);
}
}
}
}
@@ -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);
@@ -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);
}
@@ -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());
}
}