Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Comment thread
mansi75 marked this conversation as resolved.
}
return collectionData;
Expand All @@ -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));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain when loanProduct is required in calculateAvailableDisbursementAmountWithOverApplied function ? When loanProduct is not present what part of loan product it utilize when calculating AvailableDisburementAmountWithOverApplied

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on Adam's review I have one more concern. If loan product is non nullable and giving NPE shouldn't we use fail fast approach in this? As you have applied the npe handling in calculation wouldn't it cause logical issues? Even through all tests are passing.

}
collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
PossibleNextRepaymentCalculationService possibleNextRepaymentCalculationService = possibleNextRepaymentCalculationServiceDiscovery
.getService(loan);
Expand Down Expand Up @@ -212,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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,56 @@

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;
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.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)
class DelinquencyReadPlatformServiceImplTest {

@Mock
private DelinquencyRangeRepository repositoryRange;

@Mock
private DelinquencyBucketRepository repositoryBucket;
@Mock
Expand All @@ -59,21 +78,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
private ConfigurationDomainService configurationDomainService;
@Mock
private LoanTransactionRepository loanTransactionRepository;
@Mock
private PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationServiceDiscovery;
@Mock
private DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;

@InjectMocks
private DelinquencyReadPlatformServiceImpl underTest;
Expand Down Expand Up @@ -157,6 +179,55 @@ public void testMultiplePausesWithoutResumeActionCurrentBusinessDateBetweenStart
);
}

@Test
void givenPendingLoanWithNullProduct_whenCalculateLoanCollectionData_thenNoExceptionAndOverAppliedIsNull() {
Loan loan = mock(Loan.class);
when(loan.getLoanProduct()).thenReturn(null);
when(loan.isSubmittedAndPendingApproval()).thenReturn(true);
// 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()).isEqualByComparingTo(BigDecimal.ZERO);
}

@Test
void givenActiveLoanWithNullProduct_whenCalculateLoanCollectionData_thenNoExceptionAndOverAppliedIsNull() {
HashMap<BusinessDateType, LocalDate> 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);

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));

CollectionData result = underTest.calculateLoanCollectionData(1L);

assertThat(result).isNotNull();
assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isEqualByComparingTo(BigDecimal.ZERO);
} finally {
ThreadLocalContextUtil.reset();
}
}

@Test
public void testMultiplePausesWithoutResumeCurrentBusinessDateIsNotOverlappingWithAnyOfThePauses() {
// given
Expand All @@ -180,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<MoneyHelper> 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());
Expand Down
Loading