diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index 21ea7ec3279..5850b8a4b4a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -529,7 +529,7 @@ private BigDecimal calcInterestTransactionWaivedAmount(@NonNull LoanRepaymentSch Predicate transactionPredicate = t -> !t.isReversed() && t.isInterestWaiver() && !DateUtils.isAfter(t.getTransactionDate(), tillDate); return installment.getLoanTransactionToRepaymentScheduleMappings().stream() - .filter(tm -> transactionPredicate.test(tm.getLoanTransaction())) + .filter(tm -> tm.getLoanTransaction() != null && transactionPredicate.test(tm.getLoanTransaction())) .map(LoanTransactionToRepaymentScheduleMapping::getInterestPortion).reduce(BigDecimal.ZERO, MathUtil::add); } @@ -639,7 +639,7 @@ private BigDecimal calcChargeWaivedAmount(@NonNull final Collection { final LoanTransaction t = pb.getLoanTransaction(); - return !t.isReversed() && t.isWaiveCharge() && !DateUtils.isAfter(t.getTransactionDate(), tillDate); + return t != null && !t.isReversed() && t.isWaiveCharge() && !DateUtils.isAfter(t.getTransactionDate(), tillDate); }).map(LoanChargePaidBy::getAmount).reduce(BigDecimal.ZERO, MathUtil::add); } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImplTest.java index 58ea369393a..777767ff2ff 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImplTest.java @@ -18,22 +18,36 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +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.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.ZoneId; +import java.util.List; +import java.util.Set; import java.util.stream.Stream; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -43,6 +57,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -71,6 +86,15 @@ void setUp() { when(loan.isClosed()).thenReturn(false); when(loan.getStatus()).thenReturn(loanStatus); when(loanStatus.isOverpaid()).thenReturn(false); + + ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "test", "Test Tenant", "America/Mexico_City", null)); + MoneyHelper.initializeTenantRoundingMode("test", 6); + } + + @AfterEach + void tearDown() { + ThreadLocalContextUtil.reset(); + MoneyHelper.clearCache(); } @ParameterizedTest @@ -95,6 +119,54 @@ void addPeriodicAccruals_ShouldNotProceed_WhenLoanIsClosedOrOverpaid(final boole verify(loan, never()).addLoanTransaction(any()); } + @Test + void calcInterestTransactionWaivedAmount_ShouldSkipMappingsWithoutTransaction() { + // Given + LocalDate tillDate = LocalDate.now(ZoneId.systemDefault()); + LoanRepaymentScheduleInstallment installment = mock(LoanRepaymentScheduleInstallment.class); + LoanTransactionToRepaymentScheduleMapping nullTransactionMapping = mock(LoanTransactionToRepaymentScheduleMapping.class); + LoanTransactionToRepaymentScheduleMapping interestWaiverMapping = mock(LoanTransactionToRepaymentScheduleMapping.class); + LoanTransaction interestWaiverTransaction = mock(LoanTransaction.class); + + when(nullTransactionMapping.getLoanTransaction()).thenReturn(null); + when(interestWaiverMapping.getLoanTransaction()).thenReturn(interestWaiverTransaction); + when(interestWaiverMapping.getInterestPortion()).thenReturn(new BigDecimal("12.34")); + when(interestWaiverTransaction.isReversed()).thenReturn(false); + when(interestWaiverTransaction.isInterestWaiver()).thenReturn(true); + when(interestWaiverTransaction.getTransactionDate()).thenReturn(tillDate); + when(installment.getLoanTransactionToRepaymentScheduleMappings()).thenReturn(Set.of(nullTransactionMapping, interestWaiverMapping)); + + // When + BigDecimal result = ReflectionTestUtils.invokeMethod(accrualsProcessingService, "calcInterestTransactionWaivedAmount", installment, + tillDate); + + // Then + assertThat(result).isEqualByComparingTo("12.34"); + } + + @Test + void calcChargeWaivedAmount_ShouldSkipMappingsWithoutTransaction() { + // Given + LocalDate tillDate = LocalDate.now(ZoneId.systemDefault()); + LoanChargePaidBy nullTransactionPaidBy = mock(LoanChargePaidBy.class); + LoanChargePaidBy chargeWaiverPaidBy = mock(LoanChargePaidBy.class); + LoanTransaction chargeWaiverTransaction = mock(LoanTransaction.class); + + when(nullTransactionPaidBy.getLoanTransaction()).thenReturn(null); + when(chargeWaiverPaidBy.getLoanTransaction()).thenReturn(chargeWaiverTransaction); + when(chargeWaiverPaidBy.getAmount()).thenReturn(new BigDecimal("12.34")); + when(chargeWaiverTransaction.isReversed()).thenReturn(false); + when(chargeWaiverTransaction.isWaiveCharge()).thenReturn(true); + when(chargeWaiverTransaction.getTransactionDate()).thenReturn(tillDate); + + // When + BigDecimal result = ReflectionTestUtils.invokeMethod(accrualsProcessingService, "calcChargeWaivedAmount", + List.of(nullTransactionPaidBy, chargeWaiverPaidBy), tillDate); + + // Then + assertThat(result).isEqualByComparingTo("12.34"); + } + private static Stream loanStatusTestCases() { return Stream.of(Arguments.of(true, false), // Loan is closed Arguments.of(false, true) // Loan is overpaid