From 37fa791f1eeb8d1c040543fbb07d1402488856f1 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Tue, 28 Apr 2026 12:16:41 +0100 Subject: [PATCH 1/5] FINERACT-2593: Fix inverted null guard and add self-defensive NPE protection in DelinquencyReadPlatformServiceImpl --- .../DelinquencyReadPlatformServiceImpl.java | 6 ++++-- .../DelinquencyReadPlatformServiceImplTest.java | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index a49b5dee7f5..2e484ba7bf9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -156,7 +156,7 @@ public CollectionData calculateLoanCollectionData(final Long loanId) { // If the Loan is not Active yet or is cancelled (rejected or withdrawn), return template data if (loan.isSubmittedAndPendingApproval() || loan.isApproved() || loan.isCancelled()) { - if (loan.getLoanProduct() == null || loan.getLoanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { + if (loan.getLoanProduct() != null && loan.getLoanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan)); } return collectionData; @@ -173,7 +173,9 @@ public CollectionData calculateLoanCollectionData(final Long loanId) { // loans collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList); collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan)); - collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan)); + if (loan.getLoanProduct() != null) { + collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan)); + } collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan)); PossibleNextRepaymentCalculationService possibleNextRepaymentCalculationService = possibleNextRepaymentCalculationServiceDiscovery .getService(loan); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java index dd8d3c01c00..08c02159a82 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java @@ -25,6 +25,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; + +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction; @@ -38,6 +40,7 @@ import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -75,6 +78,18 @@ class DelinquencyReadPlatformServiceImplTest { @Mock private LoanDelinquencyActionRepository loanDelinquencyActionRepository; + @Mock + private ConfigurationDomainService configurationDomainService; + + @Mock + private LoanTransactionRepository loanTransactionRepository; + + @Mock + private PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationServiceDiscovery; + + @Mock + private LoanDelinquencyActionRepository loanDelinquencyActionRepository; + @InjectMocks private DelinquencyReadPlatformServiceImpl underTest; From 3264fbce55d516ea6287ec2cb8c93c476f701414 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Tue, 28 Apr 2026 12:27:37 +0100 Subject: [PATCH 2/5] FINERACT-2593: Fix inverted null guard and add self-defensive NPE protection in DelinquencyReadPlatformServiceImpl --- .../service/DelinquencyReadPlatformServiceImplTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java index 08c02159a82..884d8f2bb2b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java @@ -87,8 +87,6 @@ class DelinquencyReadPlatformServiceImplTest { @Mock private PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationServiceDiscovery; - @Mock - private LoanDelinquencyActionRepository loanDelinquencyActionRepository; @InjectMocks private DelinquencyReadPlatformServiceImpl underTest; From 0655b4778d67cde4a556c9dcca8cae4903c9a09c Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Tue, 28 Apr 2026 12:44:35 +0100 Subject: [PATCH 3/5] =?UTF-8?q?FINERACT-2593:=20Address=20Copilot=20review?= =?UTF-8?q?=20=E2=80=94=20add=20DelinquencyEffectivePauseHelper=20mock,=20?= =?UTF-8?q?fix=20import=20formatting,=20add=20null-product=20regression=20?= =?UTF-8?q?tests,=20extend=20active-loan=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DelinquencyReadPlatformServiceImpl.java | 2 +- ...elinquencyReadPlatformServiceImplTest.java | 53 ++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index 2e484ba7bf9..82263d0bbef 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -173,7 +173,7 @@ public CollectionData calculateLoanCollectionData(final Long loanId) { // loans collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList); collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan)); - if (loan.getLoanProduct() != null) { + if (loan.getLoanProduct() != null && loan.getLoanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan)); } collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan)); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java index 884d8f2bb2b..4a2e288fa85 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java @@ -20,12 +20,14 @@ import static java.time.Month.JANUARY; import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.PAUSE; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.time.LocalDate; import java.util.Arrays; import java.util.Collection; import java.util.List; - import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; @@ -33,12 +35,14 @@ import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository; +import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper; import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper; import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper; import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper; import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData; import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.junit.jupiter.api.Assertions; @@ -53,7 +57,6 @@ class DelinquencyReadPlatformServiceImplTest { @Mock private DelinquencyRangeRepository repositoryRange; - @Mock private DelinquencyBucketRepository repositoryBucket; @Mock @@ -62,31 +65,24 @@ class DelinquencyReadPlatformServiceImplTest { private DelinquencyRangeMapper mapperRange; @Mock private DelinquencyBucketMapper mapperBucket; - @Mock private LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory; - @Mock private LoanRepository loanRepository; - @Mock private LoanDelinquencyDomainService loanDelinquencyDomainService; - @Mock private LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag; - @Mock private LoanDelinquencyActionRepository loanDelinquencyActionRepository; - - @Mock + @Mock private ConfigurationDomainService configurationDomainService; - - @Mock + @Mock private LoanTransactionRepository loanTransactionRepository; - @Mock private PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationServiceDiscovery; - + @Mock + private DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; @InjectMocks private DelinquencyReadPlatformServiceImpl underTest; @@ -170,6 +166,37 @@ public void testMultiplePausesWithoutResumeActionCurrentBusinessDateBetweenStart ); } + @Test + void givenPendingLoanWithNullProduct_whenCalculateLoanCollectionData_thenNoExceptionAndOverAppliedIsNull() { + Loan loan = mock(Loan.class); + when(loan.getLoanProduct()).thenReturn(null); + when(loan.isSubmittedAndPendingApproval()).thenReturn(true); + when(loan.isApproved()).thenReturn(false); + when(loan.isCancelled()).thenReturn(false); + when(loanRepository.findById(1L)).thenReturn(Optional.of(loan)); + + CollectionData result = underTest.calculateLoanCollectionData(1L); + + assertThat(result).isNotNull(); + assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isNull(); + } + + @Test + void givenActiveLoanWithNullProduct_whenCalculateLoanCollectionData_thenNoExceptionAndOverAppliedIsNull() { + Loan loan = mock(Loan.class); + when(loan.getLoanProduct()).thenReturn(null); + when(loan.isSubmittedAndPendingApproval()).thenReturn(false); + when(loan.isApproved()).thenReturn(false); + when(loan.isCancelled()).thenReturn(false); + when(loanRepository.findById(1L)).thenReturn(Optional.of(loan)); + when(loanDelinquencyDomainService.getOverdueCollectionData(any(), any())).thenReturn(CollectionData.template()); + + CollectionData result = underTest.calculateLoanCollectionData(1L); + + assertThat(result).isNotNull(); + assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isNull(); + } + @Test public void testMultiplePausesWithoutResumeCurrentBusinessDateIsNotOverlappingWithAnyOfThePauses() { // given From 4d278a916a9066bab3baeae632cc85cd44689044 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Tue, 28 Apr 2026 21:57:24 +0100 Subject: [PATCH 4/5] FINERACT-2593: Add null guard in helper method and direct unit tests for calculateAvailableDisbursementAmountWithOverApplied --- .../DelinquencyReadPlatformServiceImpl.java | 2 +- ...elinquencyReadPlatformServiceImplTest.java | 113 ++++++++++++++++-- 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index 82263d0bbef..be07d7d9d29 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -214,7 +214,7 @@ public BigDecimal calculateAvailableDisbursementAmountWithOverApplied(@NonNull f BigDecimal approvedWithOverApplied = loan.getApprovedPrincipal(); // If over applied amount is enabled, calculate the maximum allowed amount - if (loanProduct.isAllowApprovedDisbursedAmountsOverApplied()) { + if (loanProduct != null && loanProduct.isAllowApprovedDisbursedAmountsOverApplied()) { if (loanProduct.getOverAppliedCalculationType() != null) { if ("percentage".equalsIgnoreCase(loanProduct.getOverAppliedCalculationType())) { final BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber()); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java index 4a2e288fa85..53ad01cfe82 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java @@ -20,15 +20,25 @@ import static java.time.Month.JANUARY; import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.PAUSE; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; import java.time.LocalDate; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Optional; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction; @@ -45,11 +55,14 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -171,30 +184,48 @@ void givenPendingLoanWithNullProduct_whenCalculateLoanCollectionData_thenNoExcep Loan loan = mock(Loan.class); when(loan.getLoanProduct()).thenReturn(null); when(loan.isSubmittedAndPendingApproval()).thenReturn(true); - when(loan.isApproved()).thenReturn(false); - when(loan.isCancelled()).thenReturn(false); + // REMOVED: when(loan.isApproved()).thenReturn(false); ← unnecessary + // REMOVED: when(loan.isCancelled()).thenReturn(false); ← unnecessary when(loanRepository.findById(1L)).thenReturn(Optional.of(loan)); CollectionData result = underTest.calculateLoanCollectionData(1L); assertThat(result).isNotNull(); - assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isNull(); + assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isEqualByComparingTo(BigDecimal.ZERO); } @Test void givenActiveLoanWithNullProduct_whenCalculateLoanCollectionData_thenNoExceptionAndOverAppliedIsNull() { - Loan loan = mock(Loan.class); - when(loan.getLoanProduct()).thenReturn(null); - when(loan.isSubmittedAndPendingApproval()).thenReturn(false); - when(loan.isApproved()).thenReturn(false); - when(loan.isCancelled()).thenReturn(false); - when(loanRepository.findById(1L)).thenReturn(Optional.of(loan)); - when(loanDelinquencyDomainService.getOverdueCollectionData(any(), any())).thenReturn(CollectionData.template()); + HashMap businessDates = new HashMap<>(); + businessDates.put(BusinessDateType.BUSINESS_DATE, LocalDate.of(2024, 1, 1)); + businessDates.put(BusinessDateType.COB_DATE, LocalDate.of(2024, 1, 1)); + ThreadLocalContextUtil.setBusinessDates(businessDates); - CollectionData result = underTest.calculateLoanCollectionData(1L); + try { + Loan loan = mock(Loan.class); + when(loan.getLoanProduct()).thenReturn(null); + when(loan.isSubmittedAndPendingApproval()).thenReturn(false); + when(loan.isApproved()).thenReturn(false); + when(loan.isCancelled()).thenReturn(false); + when(loan.getApprovedPrincipal()).thenReturn(BigDecimal.valueOf(10000)); + when(loan.getDisbursedAmount()).thenReturn(BigDecimal.valueOf(5000)); + when(loan.getLoanRepaymentScheduleDetail()).thenReturn(mock(LoanProductRelatedDetail.class)); + when(loanDelinquencyDomainService.getOverdueCollectionData(any(), any())).thenReturn(CollectionData.template()); + when(loanDelinquencyActionRepository.findByLoanOrderById(any())).thenReturn(List.of()); + when(configurationDomainService.getNextPaymentDateConfigForLoan()).thenReturn(null); + when(possibleNextRepaymentCalculationServiceDiscovery.getService(any())).thenReturn(null); + when(loan.getLastPaymentTransaction()).thenReturn(null); + when(loan.getLastRepaymentOrDownPaymentTransaction()).thenReturn(null); + when(loan.isEnableInstallmentLevelDelinquency()).thenReturn(false); + when(loanRepository.findById(1L)).thenReturn(Optional.of(loan)); - assertThat(result).isNotNull(); - assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isNull(); + CollectionData result = underTest.calculateLoanCollectionData(1L); + + assertThat(result).isNotNull(); + assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isEqualByComparingTo(BigDecimal.ZERO); + } finally { + ThreadLocalContextUtil.reset(); + } } @Test @@ -220,6 +251,62 @@ public void testMultiplePausesWithoutResumeCurrentBusinessDateIsNotOverlappingWi ); } + @Test + void givenLoanWithNullProduct_whenHelperCalledDirectly_thenReturnsZero() { + Loan loan = mock(Loan.class); + when(loan.getLoanProduct()).thenReturn(null); + when(loan.getApprovedPrincipal()).thenReturn(BigDecimal.valueOf(10000)); + when(loan.getDisbursedAmount()).thenReturn(BigDecimal.valueOf(5000)); + when(loan.getLoanRepaymentScheduleDetail()).thenReturn(mock(LoanProductRelatedDetail.class)); + + BigDecimal result = underTest.calculateAvailableDisbursementAmountWithOverApplied(loan); + + assertThat(result).isEqualByComparingTo(BigDecimal.valueOf(5000)); + } + + @Test + void givenLoanWithProductOverApplyDisabled_whenHelperCalledDirectly_thenReturnsApprovedMinusDisbursed() { + Loan loan = mock(Loan.class); + LoanProduct loanProduct = mock(LoanProduct.class); + when(loan.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.isAllowApprovedDisbursedAmountsOverApplied()).thenReturn(false); + when(loan.getApprovedPrincipal()).thenReturn(BigDecimal.valueOf(10000)); + when(loan.getDisbursedAmount()).thenReturn(BigDecimal.valueOf(4000)); + when(loan.getLoanRepaymentScheduleDetail()).thenReturn(mock(LoanProductRelatedDetail.class)); + + BigDecimal result = underTest.calculateAvailableDisbursementAmountWithOverApplied(loan); + + assertThat(result).isEqualByComparingTo(BigDecimal.valueOf(6000)); + } + + @Test + void givenLoanWithPercentageOverApply_whenHelperCalledDirectly_thenReturnsCalculatedAmount() { + // MoneyHelper.getMathContext() requires a tenant context + MathContext mathContext = new MathContext(19, RoundingMode.HALF_EVEN); + MockedStatic moneyHelperMock = mockStatic(MoneyHelper.class); + moneyHelperMock.when(MoneyHelper::getMathContext).thenReturn(mathContext); + + try { + Loan loan = mock(Loan.class); + LoanProduct loanProduct = mock(LoanProduct.class); + when(loan.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.isAllowApprovedDisbursedAmountsOverApplied()).thenReturn(true); + when(loanProduct.getOverAppliedCalculationType()).thenReturn("percentage"); + when(loanProduct.getOverAppliedNumber()).thenReturn(10); + when(loan.getProposedPrincipal()).thenReturn(BigDecimal.valueOf(10000)); + when(loan.getApprovedPrincipal()).thenReturn(BigDecimal.valueOf(10000)); + when(loan.getDisbursedAmount()).thenReturn(BigDecimal.ZERO); + when(loan.getLoanRepaymentScheduleDetail()).thenReturn(mock(LoanProductRelatedDetail.class)); + + BigDecimal result = underTest.calculateAvailableDisbursementAmountWithOverApplied(loan); + + // 10000 * (1 + 10/100) = 11000, minus 0 disbursed = 11000 + assertThat(result).isEqualByComparingTo(BigDecimal.valueOf(11000)); + } finally { + moneyHelperMock.close(); + } + } + private void verifyPausePeriods(CollectionData collectionData, DelinquencyPausePeriod... pausePeriods) { if (pausePeriods.length > 0) { Assertions.assertEquals(Arrays.asList(pausePeriods), collectionData.getDelinquencyPausePeriods()); From 0c080bf2ea6d66d47e1589a6e169c4157cb71a30 Mon Sep 17 00:00:00 2001 From: mansimaurya Date: Wed, 29 Apr 2026 15:26:25 +0100 Subject: [PATCH 5/5] FINERACT-2593: Fix active-loan branch to call helper unconditionally when LoanProduct non-null --- .../delinquency/service/DelinquencyReadPlatformServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index be07d7d9d29..8e878d55919 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -173,7 +173,7 @@ public CollectionData calculateLoanCollectionData(final Long loanId) { // loans collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList); collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan)); - if (loan.getLoanProduct() != null && loan.getLoanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { + if (loan.getLoanProduct() != null) { collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan)); } collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));