FINERACT-2181: Fix the interest calculation for non active loan

This commit is contained in:
Attila Budai
2025-07-31 19:59:15 +02:00
committed by Adam Saghy
parent 3e2684c0d6
commit 0762a012e5
13 changed files with 517 additions and 21 deletions
@@ -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();
}
}
@@ -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"));
}
}
}