FINERACT-2181: Fix the interest calculation for non active loan
This commit is contained in:
+5
@@ -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();
|
||||
|
||||
+15
-11
@@ -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<LoanStatus> ACTIVE_LOAN_STATUSES = List.of(ACTIVE, TRANSFER_IN_PROGRESS, TRANSFER_ON_HOLD);
|
||||
private static final List<LoanStatus> 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<ExternalTransferStatus> BUYBACK_READY_STATUSES = List.of(ExternalTransferStatus.PENDING,
|
||||
ExternalTransferStatus.ACTIVE);
|
||||
private static final List<ExternalTransferStatus> 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<LoanStatus> 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<LoanStatus> getAllowedLoanStatuses() {
|
||||
return configurationDomainService.getAllowedLoanStatusesForExternalAssetTransfer().stream().map(LoanStatus::valueOf)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<LoanStatus> getAllowedLoanStatusesForDelayedSettlement() {
|
||||
return configurationDomainService.getAllowedLoanStatusesOfDelayedSettlementForExternalAssetTransfer().stream()
|
||||
.map(LoanStatus::valueOf).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -40,4 +40,5 @@
|
||||
<include relativeToChangelogFile="true" file="parts/0016_add_external_reference_id.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/0017_add_external_asset_owner_loan_product_attr_index.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/0018_add_external_asset_owner_transfer_outstanding_interest_strategy.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/0019_add_configurable_allowed_loan_statuses.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="c_configuration">
|
||||
<column name="name" value="allowed-loan-statuses-for-external-asset-transfer"/>
|
||||
<column name="value"/>
|
||||
<column name="date_value"/>
|
||||
<column name="string_value" value="ACTIVE,TRANSFER_IN_PROGRESS,TRANSFER_ON_HOLD"/>
|
||||
<column name="enabled" valueBoolean="true"/>
|
||||
<column name="is_trap_door" valueBoolean="false"/>
|
||||
<column name="description" value=" Available options: Any combination from LoanStatus enum separated by comma."/>
|
||||
</insert>
|
||||
<insert tableName="c_configuration">
|
||||
<column name="name" value="allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer"/>
|
||||
<column name="value"/>
|
||||
<column name="date_value"/>
|
||||
<column name="string_value" value="ACTIVE,TRANSFER_IN_PROGRESS,TRANSFER_ON_HOLD,OVERPAID,CLOSED_OBLIGATIONS_MET"/>
|
||||
<column name="enabled" valueBoolean="true"/>
|
||||
<column name="is_trap_door" valueBoolean="false"/>
|
||||
<column name="description" value=" Available options: Any combination from LoanStatus enum separated by comma."/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+127
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -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<Arguments> 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"));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user