diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java index 3e6426a1d..979eea042 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java @@ -77,6 +77,8 @@ public final class GlobalConfigurationConstants { public static final String ENABLE_PAYMENT_HUB_INTEGRATION = "enable-payment-hub-integration"; public static final String ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY = "enable-immediate-charge-accrual-post-maturity"; public static final String ASSET_OWNER_TRANSFER_OUTSTANDING_INTEREST_CALCULATION_STRATEGY = "outstanding-interest-calculation-strategy-for-external-asset-transfer"; + public static final String ALLOWED_LOAN_STATUSES_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-for-external-asset-transfer"; + public static final String ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer"; private GlobalConfigurationConstants() {} } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index 7a138fb25..e7a98fb2c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -19,12 +19,17 @@ package org.apache.fineract.infrastructure.configuration.domain; import java.time.LocalDate; +import java.util.List; import org.apache.fineract.infrastructure.cache.domain.CacheType; public interface ConfigurationDomainService { boolean isMakerCheckerEnabledForTask(String taskPermissionCode); + List getAllowedLoanStatusesForExternalAssetTransfer(); + + List getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer(); + boolean isSameMakerCheckerEnabled(); boolean isAmazonS3Enabled(); diff --git a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java index eac4d0d9f..5926ed6c8 100644 --- a/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java +++ b/fineract-investor/src/main/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculation.java @@ -49,6 +49,11 @@ public class ExternalAssetOwnerTransferOutstandingInterestCalculation { } public BigDecimal calculateOutstandingInterest(Loan loan) { + // If loan is not active, there should be no outstanding interest + if (!loan.isOpen()) { + return BigDecimal.ZERO; + } + String outstandingInterestCalculationStrategy = configurationDomainService.getAssetOwnerTransferOustandingInterestStrategy(); return switch (outstandingInterestCalculationStrategy) { case "TOTAL_OUTSTANDING_INTEREST" -> loan.getSummary().getTotalInterestOutstanding(); 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 0ba4f709c..19d4be902 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 @@ -21,11 +21,6 @@ package org.apache.fineract.investor.service; import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE_INTERMEDIATE; import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING; import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING_INTERMEDIATE; -import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.ACTIVE; -import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.CLOSED_OBLIGATIONS_MET; -import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.OVERPAID; -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; @@ -43,6 +38,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.cob.data.LoanDataForExternalTransfer; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; @@ -74,9 +70,6 @@ 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 ACTIVE_LOAN_STATUSES = List.of(ACTIVE, TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD); - private static final List VALID_DELAYED_SETTLEMENT_LOAN_STATUSES_BUYBACK_AND_SALE = List.of(ACTIVE, TRANSFER_IN_PROGRESS, - TRANSFER_ON_HOLD, OVERPAID, CLOSED_OBLIGATIONS_MET); private static final List BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE); private static final List BUYBACK_READY_STATUSES_FOR_DELAY_SETTLEMENT = List @@ -86,6 +79,7 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW private final FromJsonHelper fromApiJsonHelper; private final LoanRepository loanRepository; private final DelayedSettlementAttributeService delayedSettlementAttributeService; + private final ConfigurationDomainService configurationDomainService; @Override @Transactional @@ -388,16 +382,16 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW private void validateLoanStatusIntermediarySale(LoanDataForExternalTransfer loanDataForExternalTransfer) { LoanStatus loanStatus = loanDataForExternalTransfer.getLoanStatus(); - if (!ACTIVE_LOAN_STATUSES.contains(loanStatus)) { + if (!getAllowedLoanStatuses().contains(loanStatus)) { throw new ExternalAssetOwnerInitiateTransferException(String.format("Loan status %s is not valid for transfer.", loanStatus)); } } private List getValidLoanStatusList(boolean isDelayedSettlementEnabled) { if (isDelayedSettlementEnabled) { - return VALID_DELAYED_SETTLEMENT_LOAN_STATUSES_BUYBACK_AND_SALE; + return getAllowedLoanStatusesForDelayedSettlement(); } else { - return ACTIVE_LOAN_STATUSES; + return getAllowedLoanStatuses(); } } @@ -577,4 +571,14 @@ public class ExternalAssetOwnersWriteServiceImpl implements ExternalAssetOwnersW externalAssetOwner.setExternalId(ExternalIdFactory.produce(externalId)); return externalAssetOwnerRepository.saveAndFlush(externalAssetOwner); } + + private List getAllowedLoanStatuses() { + return configurationDomainService.getAllowedLoanStatusesForExternalAssetTransfer().stream().map(LoanStatus::valueOf) + .collect(Collectors.toList()); + } + + private List getAllowedLoanStatusesForDelayedSettlement() { + return configurationDomainService.getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer().stream() + .map(LoanStatus::valueOf).collect(Collectors.toList()); + } } diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml index e96557908..5e955c664 100644 --- a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/module-changelog-master.xml @@ -40,4 +40,5 @@ + diff --git a/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0019_add_configurable_allowed_loan_statuses.xml b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0019_add_configurable_allowed_loan_statuses.xml new file mode 100644 index 000000000..552f6a421 --- /dev/null +++ b/fineract-investor/src/main/resources/db/changelog/tenant/module/investor/parts/0019_add_configurable_allowed_loan_statuses.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculationTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculationTest.java new file mode 100644 index 000000000..799d16c52 --- /dev/null +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnerTransferOutstandingInterestCalculationTest.java @@ -0,0 +1,127 @@ +/** + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; +import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; +import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.service.LoanSummaryDataProvider; +import org.apache.fineract.portfolio.loanaccount.service.LoanSummaryProviderDelegate; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ExternalAssetOwnerTransferOutstandingInterestCalculationTest { + + @Mock + private LoanSummaryProviderDelegate loanSummaryDataProvider; + + @Mock + private ConfigurationDomainService configurationDomainService; + + @Mock + private LoanReadPlatformService loanReadPlatformService; + + @Mock + private CurrencyMapper currencyMapper; + + @Mock + private Loan loan; + + @Mock + private LoanSummary loanSummary; + + @Mock + private LoanAccountData loanAccountData; + + @Mock + private LoanScheduleData loanScheduleData; + + @Mock + private LoanSummaryDataProvider summaryDataProvider; + + @InjectMocks + private ExternalAssetOwnerTransferOutstandingInterestCalculation externalAssetOwnerTransferOutstandingInterestCalculation; + + @Test + void testCalculateOutstandingInterest_WhenLoanNotDisbursed_ShouldReturnZero() { + // Given + when(loan.isOpen()).thenReturn(false); + + // When + BigDecimal result = externalAssetOwnerTransferOutstandingInterestCalculation.calculateOutstandingInterest(loan); + + // Then + assertEquals(BigDecimal.ZERO, result); + + // Verify that no other calculations were performed + verify(configurationDomainService, never()).getAssetOwnerTransferOustandingInterestStrategy(); + verify(loanReadPlatformService, never()).retrieveOne(Mockito.anyLong()); + verify(loan, times(1)).isOpen(); + } + + @Test + void testCalculateOutstandingInterest_WhenLoanDisbursedWithTotalStrategy_ShouldCalculate() { + // Given - ACTIVE loan with TOTAL_OUTSTANDING_INTEREST strategy + when(loan.isOpen()).thenReturn(true); + when(configurationDomainService.getAssetOwnerTransferOustandingInterestStrategy()).thenReturn("TOTAL_OUTSTANDING_INTEREST"); + when(loan.getSummary()).thenReturn(loanSummary); + BigDecimal expectedInterest = new BigDecimal("150.50"); + when(loanSummary.getTotalInterestOutstanding()).thenReturn(expectedInterest); + + // When + BigDecimal result = externalAssetOwnerTransferOutstandingInterestCalculation.calculateOutstandingInterest(loan); + + // Then + assertEquals(expectedInterest, result); + verify(loan, times(1)).isOpen(); + verify(loan, times(1)).getSummary(); + } + + @Test + void testCalculateOutstandingInterest_BackdatedUndisbursedLoan_ShouldReturnZero() { + // Given - backdated loan (created 3 months ago) but not disbursed + when(loan.isOpen()).thenReturn(false); + + // When + BigDecimal result = externalAssetOwnerTransferOutstandingInterestCalculation.calculateOutstandingInterest(loan); + + // Then + assertEquals(BigDecimal.ZERO, result); + + // Verify that the method returned early due to disbursement check + verify(loan, times(1)).isOpen(); + verify(configurationDomainService, never()).getAssetOwnerTransferOustandingInterestStrategy(); + } +} diff --git a/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceTest.java b/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceTest.java index fef26dd75..c058214ba 100644 --- a/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceTest.java +++ b/fineract-investor/src/test/java/org/apache/fineract/investor/service/ExternalAssetOwnersWriteServiceTest.java @@ -50,6 +50,7 @@ import java.util.Optional; import java.util.stream.Stream; import org.apache.commons.lang3.RandomStringUtils; import org.apache.fineract.cob.data.LoanDataForExternalTransfer; +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.domain.ExternalId; @@ -418,8 +419,8 @@ public class ExternalAssetOwnersWriteServiceTest { } private static Stream loanStatusValidationDataProviderInvalidDelayedSettlement() { - return Stream.of(Arguments.of("Invalid Loan Status", LoanStatus.INVALID), Arguments.of("Approved Loan Status", LoanStatus.APPROVED), - Arguments.of("Rejected Loan Status", LoanStatus.REJECTED), + return Stream.of(Arguments.of("Invalid Loan Status", LoanStatus.INVALID), Arguments.of("Rejected Loan Status", LoanStatus.REJECTED), + Arguments.of("Approved Loan Status", LoanStatus.APPROVED), Arguments.of("Submitted and Pending Approval Loan Status", LoanStatus.SUBMITTED_AND_PENDING_APPROVAL), Arguments.of("Withdrawn By Client Loan Status", LoanStatus.WITHDRAWN_BY_CLIENT), Arguments.of("Closed Written Off Loan Status", LoanStatus.CLOSED_WRITTEN_OFF), @@ -860,6 +861,9 @@ public class ExternalAssetOwnersWriteServiceTest { @Mock private LoanDataForExternalTransfer loanDataForExternalTransfer; + @Mock + private ConfigurationDomainService configurationDomainService; + @InjectMocks private ExternalAssetOwnersWriteServiceImpl externalAssetOwnersWriteServiceImpl; @@ -926,6 +930,11 @@ public class ExternalAssetOwnersWriteServiceTest { lenient().when(loanDataForExternalTransfer.getLoanStatus()).thenReturn(LoanStatus.ACTIVE); lenient().when(loanDataForExternalTransfer.getLoanProductId()).thenReturn(loanProductId); lenient().when(loanDataForExternalTransfer.getLoanProductShortName()).thenReturn(loanProductShortName); + lenient().when(configurationDomainService.getAllowedLoanStatusesForExternalAssetTransfer()) + .thenReturn(List.of("ACTIVE", "TRANSFER_IN_PROGRESS", "TRANSFER_ON_HOLD")); + lenient().when(configurationDomainService.getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer()) + .thenReturn(List.of("ACTIVE", "TRANSFER_IN_PROGRESS", "TRANSFER_ON_HOLD", "OVERPAID", "CLOSED_OBLIGATIONS_MET")); + } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index cc17cad24..26b48b2f0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -20,6 +20,7 @@ package org.apache.fineract.infrastructure.configuration.domain; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -60,6 +61,20 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService return false; } + @Override + public List getAllowedLoanStatusesForExternalAssetTransfer() { + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData( + GlobalConfigurationConstants.ALLOWED_LOAN_STATUSES_FOR_EXTERNAL_ASSET_TRANSFER); + return List.of(property.getStringValue().split(",")); + } + + @Override + public List getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer() { + final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData( + GlobalConfigurationConstants.ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER); + return List.of(property.getStringValue().split(",")); + } + @Override public boolean isSameMakerCheckerEnabled() { return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.ENABLE_SAME_MAKER_CHECKER).isEnabled(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index dcd2a834c..cd17636d5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -3315,9 +3315,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf return Optional.empty(); } - loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING, loan); - changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); - existingTransactionIds.addAll(loanTransactionRepository.findTransactionIdsByLoan(loan)); existingReversedTransactionIds.addAll(loanTransactionRepository.findReversedTransactionIdsByLoan(loan)); @@ -3359,7 +3356,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); loanBalanceService.updateLoanSummaryDerivedFields(loan); - + loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING, loan); + changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); return Optional.of(loanTransaction); } 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 c16780be2..4bbf06ed8 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 @@ -105,8 +105,8 @@ public class GlobalConfigurationHelper { ArrayList expectedGlobalConfigurations = getAllDefaultGlobalConfigurations(); GetGlobalConfigurationsResponse actualGlobalConfigurations = getAllGlobalConfigurations(); - Assertions.assertEquals(57, expectedGlobalConfigurations.size()); - Assertions.assertEquals(57, actualGlobalConfigurations.getGlobalConfiguration().size()); + Assertions.assertEquals(59, expectedGlobalConfigurations.size()); + Assertions.assertEquals(59, actualGlobalConfigurations.getGlobalConfiguration().size()); for (int i = 0; i < expectedGlobalConfigurations.size(); i++) { @@ -549,6 +549,25 @@ public class GlobalConfigurationHelper { assetOwnerTransferInterestOutstandingStrategy.put("string_value", "TOTAL_OUTSTANDING_INTEREST"); defaults.add(assetOwnerTransferInterestOutstandingStrategy); + HashMap allowedLoanStatusesForExternalAssetTransfer = new HashMap<>(); + allowedLoanStatusesForExternalAssetTransfer.put("name", + GlobalConfigurationConstants.ALLOWED_LOAN_STATUSES_FOR_EXTERNAL_ASSET_TRANSFER); + allowedLoanStatusesForExternalAssetTransfer.put("value", 0L); + allowedLoanStatusesForExternalAssetTransfer.put("enabled", true); + allowedLoanStatusesForExternalAssetTransfer.put("trapDoor", false); + allowedLoanStatusesForExternalAssetTransfer.put("string_value", "ACTIVE,TRANSFER_IN_PROGRESS,TRANSFER_ON_HOLD"); + defaults.add(allowedLoanStatusesForExternalAssetTransfer); + + HashMap allowedLoanStatusesForDelayedSettlementExternalAssetTransfer = new HashMap<>(); + allowedLoanStatusesForDelayedSettlementExternalAssetTransfer.put("name", + GlobalConfigurationConstants.ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER); + allowedLoanStatusesForDelayedSettlementExternalAssetTransfer.put("value", 0L); + allowedLoanStatusesForDelayedSettlementExternalAssetTransfer.put("enabled", true); + allowedLoanStatusesForDelayedSettlementExternalAssetTransfer.put("trapDoor", false); + allowedLoanStatusesForDelayedSettlementExternalAssetTransfer.put("string_value", + "ACTIVE,TRANSFER_IN_PROGRESS,TRANSFER_ON_HOLD,OVERPAID,CLOSED_OBLIGATIONS_MET"); + defaults.add(allowedLoanStatusesForDelayedSettlementExternalAssetTransfer); + return defaults; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferUndisbursedLoanTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferUndisbursedLoanTest.java new file mode 100644 index 000000000..83d5b30fd --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/investor/externalassetowner/ExternalAssetOwnerTransferUndisbursedLoanTest.java @@ -0,0 +1,264 @@ +/** + * 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.integrationtests.investor.externalassetowner; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +import java.math.BigDecimal; +import java.util.UUID; +import org.apache.fineract.client.models.ExternalAssetOwnerRequest; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostInitiateTransferResponse; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.client.models.PutGlobalConfigurationsRequest; +import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; +import org.apache.fineract.integrationtests.BaseLoanIntegrationTest; +import org.apache.fineract.integrationtests.common.BusinessStepHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.ExternalAssetOwnerHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.FinancialActivityAccountHelper; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith({ LoanTestLifecycleExtension.class }) +public class ExternalAssetOwnerTransferUndisbursedLoanTest extends BaseLoanIntegrationTest { + + private Long loan1Id; + private Long loan2Id; + + @BeforeAll + public static void setup() { + new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", + "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES", + "EXTERNAL_ASSET_OWNER_TRANSFER"); + new GlobalConfigurationHelper().updateGlobalConfiguration( + GlobalConfigurationConstants.ALLOWED_LOAN_STATUSES_FOR_EXTERNAL_ASSET_TRANSFER, + new PutGlobalConfigurationsRequest().stringValue("APPROVED,ACTIVE,TRANSFER_IN_PROGRESS,TRANSFER_ON_HOLD")); + } + + @AfterAll + public static void tearDown() { + new GlobalConfigurationHelper().updateGlobalConfiguration( + GlobalConfigurationConstants.ALLOWED_LOAN_STATUSES_FOR_EXTERNAL_ASSET_TRANSFER, + new PutGlobalConfigurationsRequest().stringValue("ACTIVE,TRANSFER_IN_PROGRESS,TRANSFER_ON_HOLD")); + } + + @Test + public void testExternalAssetOwnerTransferForUndisbursedLoan() { + globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID, true); + + Account transferAccount = accountHelper.createAssetAccount(); + FinancialActivityAccountHelper financialActivityAccountHelper = new FinancialActivityAccountHelper(requestSpec); + ExternalAssetOwnerHelper externalAssetOwnerHelper = new ExternalAssetOwnerHelper(); + externalAssetOwnerHelper.setProperFinancialActivity(financialActivityAccountHelper, transferAccount); + + try { + runAt("01 January 2024", () -> { + PostClientsResponse client = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + Long clientId = client.getClientId(); + + PostLoanProductsRequest loanProductRequest = createOnePeriod30DaysPeriodicAccrualProduct(12.0) + .name(Utils.uniqueRandomStringGenerator("UNDISBURSED_TEST_", 4)) + .shortName(Utils.uniqueRandomStringGenerator("UT", 2)); + + PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(loanProductRequest); + + PostLoansResponse loanResponse = loanTransactionHelper + .applyLoan(applyLoanRequest(clientId, loanProduct.getResourceId(), "01 January 2024", 10000.0, 4)); + Long loanId = loanResponse.getLoanId(); + + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(10000.0, "01 January 2024")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + assertNotNull(loanDetails); + assertEquals("loanStatusType.approved", loanDetails.getStatus().getCode()); + + String transferExternalId = UUID.randomUUID().toString(); + String ownerExternalId = UUID.randomUUID().toString(); + + PostInitiateTransferResponse transferResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanId, "sale", + new ExternalAssetOwnerRequest().settlementDate("01 January 2024").dateFormat("dd MMMM yyyy").locale("en") + .transferExternalId(transferExternalId).ownerExternalId(ownerExternalId).purchasePriceRatio("1.0")); + + assertNotNull(transferResponse); + assertEquals(transferExternalId, transferResponse.getResourceExternalId()); + + GetLoansLoanIdResponse loanAfterTransfer = loanTransactionHelper.getLoanDetails(loanId); + assertNotNull(loanAfterTransfer, "Loan details should not be null"); + + if (loanAfterTransfer.getSummary() == null) { + assertEquals("loanStatusType.approved", loanAfterTransfer.getStatus().getCode(), + "Loan should remain in approved status"); + + assertEquals(0, BigDecimal.valueOf(10000.0).compareTo(loanAfterTransfer.getApprovedPrincipal()), + "Approved principal should be 10000"); + + return; + } + + fail("Unexpected: Loan summary should be null for undisbursed loans"); + }); + } finally { + globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID, false); + } + } + + @Test + public void testExternalAssetOwnerTransferForBackdatedUndisbursedLoan() { + globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID, true); + + Account transferAccount = accountHelper.createAssetAccount(); + FinancialActivityAccountHelper financialActivityAccountHelper = new FinancialActivityAccountHelper(requestSpec); + ExternalAssetOwnerHelper externalAssetOwnerHelper = new ExternalAssetOwnerHelper(); + externalAssetOwnerHelper.setProperFinancialActivity(financialActivityAccountHelper, transferAccount); + + try { + runAt("01 March 2024", () -> { + PostClientsResponse client = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + Long clientId = client.getClientId(); + + PostLoanProductsRequest loanProductRequest = createOnePeriod30DaysPeriodicAccrualProduct(12.0) + .name(Utils.uniqueRandomStringGenerator("BACKDATED_UNDISBURSED_", 4)) + .shortName(Utils.uniqueRandomStringGenerator("BU", 2)); + + PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(loanProductRequest); + + PostLoansResponse loanResponse = loanTransactionHelper + .applyLoan(applyLoanRequest(clientId, loanProduct.getResourceId(), "01 December 2023", 15000.0, 4)); + Long loanId = loanResponse.getLoanId(); + + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(15000.0, "01 December 2023")); + + String transferExternalId = UUID.randomUUID().toString(); + String ownerExternalId = UUID.randomUUID().toString(); + + PostInitiateTransferResponse transferResponse = externalAssetOwnerHelper.initiateTransferByLoanId(loanId, "sale", + new ExternalAssetOwnerRequest().settlementDate("01 March 2024").dateFormat("dd MMMM yyyy").locale("en") + .transferExternalId(transferExternalId).ownerExternalId(ownerExternalId).purchasePriceRatio("1.0")); + + assertNotNull(transferResponse); + assertEquals(transferExternalId, transferResponse.getResourceExternalId()); + + GetLoansLoanIdResponse loanAfterTransfer = loanTransactionHelper.getLoanDetails(loanId); + assertNotNull(loanAfterTransfer, "Loan details should not be null"); + + if (loanAfterTransfer.getSummary() == null) { + assertEquals("loanStatusType.approved", loanAfterTransfer.getStatus().getCode(), + "Loan should remain in approved status"); + + assertEquals(0, BigDecimal.valueOf(15000.0).compareTo(loanAfterTransfer.getApprovedPrincipal()), + "Approved principal should be 15000"); + + return; + } + + fail("Unexpected: Loan summary should be null for undisbursed loans"); + }); + } finally { + globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID, false); + } + } + + @Test + public void testExternalAssetOwnerTransferComparison_DisbursedVsUndisbursed() { + globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID, true); + + Account transferAccount = accountHelper.createAssetAccount(); + FinancialActivityAccountHelper financialActivityAccountHelper = new FinancialActivityAccountHelper(requestSpec); + ExternalAssetOwnerHelper externalAssetOwnerHelper = new ExternalAssetOwnerHelper(); + externalAssetOwnerHelper.setProperFinancialActivity(financialActivityAccountHelper, transferAccount); + + try { + runAt("01 January 2024", () -> { + PostClientsResponse client1 = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + PostClientsResponse client2 = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + PostLoanProductsRequest loanProductRequest = createOnePeriod30DaysPeriodicAccrualProduct(12.0) + .name(Utils.uniqueRandomStringGenerator("COMPARISON_TEST_", 4)) + .shortName(Utils.uniqueRandomStringGenerator("CT", 2)); + + PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(loanProductRequest); + + PostLoansResponse loan1Response = loanTransactionHelper + .applyLoan(applyLoanRequest(client1.getClientId(), loanProduct.getResourceId(), "01 January 2024", 20000.0, 4)); + loan1Id = loan1Response.getLoanId(); + + PostLoansResponse loan2Response = loanTransactionHelper + .applyLoan(applyLoanRequest(client2.getClientId(), loanProduct.getResourceId(), "01 January 2024", 20000.0, 4)); + loan2Id = loan2Response.getLoanId(); + + loanTransactionHelper.approveLoan(loan1Id, approveLoanRequest(20000.0, "01 January 2024")); + loanTransactionHelper.approveLoan(loan2Id, approveLoanRequest(20000.0, "01 January 2024")); + + disburseLoan(loan1Id, BigDecimal.valueOf(20000.0), "01 January 2024"); + }); + + runAt("31 January 2024", () -> { + executeInlineCOB(loan1Id); + executeInlineCOB(loan2Id); + + String transfer1ExternalId = UUID.randomUUID().toString(); + String transfer2ExternalId = UUID.randomUUID().toString(); + String ownerExternalId = UUID.randomUUID().toString(); + + PostInitiateTransferResponse transfer1Response = externalAssetOwnerHelper.initiateTransferByLoanId(loan1Id, "sale", + new ExternalAssetOwnerRequest().settlementDate("31 January 2024").dateFormat("dd MMMM yyyy").locale("en") + .transferExternalId(transfer1ExternalId).ownerExternalId(ownerExternalId).purchasePriceRatio("1.0")); + + PostInitiateTransferResponse transfer2Response = externalAssetOwnerHelper.initiateTransferByLoanId(loan2Id, "sale", + new ExternalAssetOwnerRequest().settlementDate("31 January 2024").dateFormat("dd MMMM yyyy").locale("en") + .transferExternalId(transfer2ExternalId).ownerExternalId(ownerExternalId).purchasePriceRatio("1.0")); + + GetLoansLoanIdResponse disbursedLoan = loanTransactionHelper.getLoanDetails(loan1Id); + GetLoansLoanIdResponse undisbursedLoan = loanTransactionHelper.getLoanDetails(loan2Id); + + assertNotNull(disbursedLoan, "Disbursed loan details should not be null"); + assertNotNull(undisbursedLoan, "Undisbursed loan details should not be null"); + + assertNotNull(disbursedLoan.getSummary(), "Disbursed loan summary should not be null"); + + if (undisbursedLoan.getSummary() == null) { + assertEquals("loanStatusType.active", disbursedLoan.getStatus().getCode(), "Disbursed loan should be active"); + assertEquals("loanStatusType.approved", undisbursedLoan.getStatus().getCode(), + "Undisbursed loan should remain approved"); + + BigDecimal disbursedInterest = disbursedLoan.getSummary().getInterestOutstanding(); + assertNotNull(disbursedInterest, "Disbursed loan interest outstanding should not be null"); + + return; + } + + fail("Unexpected: Undisbursed loan should not have summary data"); + }); + } finally { + globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_AUTO_GENERATED_EXTERNAL_ID, false); + } + } +} 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 15e9aca31..86720e42e 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 @@ -698,8 +698,8 @@ public class InitiateExternalAssetOwnerTransferTest extends BaseLoanIntegrationT 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"), + "2020-03-05", "2020-03-05", true, new BigDecimal("0.000000"), new BigDecimal("0.000000"), + new BigDecimal("0.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"), new BigDecimal("0.000000"))); getAndValidateThereIsNoActiveMapping(saleTransferResponse.getResourceExternalId()); } finally {