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% rename from fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/data/GlobalConfigurationData.java rename 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-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformService.java new file mode 100644 index 000000000..eb97c6844 --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerLoanStatusChangePlatformService.java @@ -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 {} 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 { + + @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 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 ACTIVE_LOAN_STATUSES = List.of(ACTIVE, TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD); private static final List 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-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferService.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferService.java new file mode 100644 index 000000000..d51fb819b --- /dev/null +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/LoanAccountOwnerTransferService.java @@ -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); +} 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 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 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 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 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 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> 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 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> 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 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> businessEventArgumentCaptor = verifyBusinessEvents(2); + verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, pendingBuybackTransfer); + verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing); + } + + @NotNull + private ArgumentCaptor> verifyBusinessEvents(int expectedBusinessEvents) { + @SuppressWarnings("unchecked") + ArgumentCaptor> businessEventArgumentCaptor = ArgumentCaptor.forClass(BusinessEvent.class); + verify(businessEventNotifierService, times(expectedBusinessEvents)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture()); + return businessEventArgumentCaptor; + } + + private void verifyLoanTransferBusinessEvent(ArgumentCaptor> 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> 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, 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 @@ + 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 @@ + + + + + + SELECT SETVAL('c_configuration_id_seq', COALESCE(MAX(id), 0)+1, false ) FROM c_configuration; + + + + + + + + + + + + + + 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 expectedGlobalConfigurations = getAllDefaultGlobalConfigurations(); ArrayList 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 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()