Skip to content
Draft
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().isAllowApprovedDisbursedAmountsOverApplied()) {
collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan));
Comment thread
mansi75 marked this conversation as resolved.
}
return collectionData;
Expand All @@ -174,6 +174,7 @@ public CollectionData calculateLoanCollectionData(final Long loanId) {
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan));
collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan));

collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
PossibleNextRepaymentCalculationService possibleNextRepaymentCalculationService = possibleNextRepaymentCalculationServiceDiscovery
.getService(loan);
Expand Down Expand Up @@ -212,7 +213,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,52 @@

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.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.List;
import java.util.Optional;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
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 +74,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 ConfigurationDomainService configurationDomainService;
@Mock
private LoanTransactionRepository loanTransactionRepository;
@Mock
private PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationServiceDiscovery;
@Mock
private LoanDelinquencyActionRepository loanDelinquencyActionRepository;
@Mock
private DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;

@InjectMocks
private DelinquencyReadPlatformServiceImpl underTest;
Expand Down Expand Up @@ -180,6 +198,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 All @@ -192,4 +266,44 @@ private DelinquencyPausePeriod pausePeriod(boolean active, String startDate, Str
return new DelinquencyPausePeriod(active, LocalDate.parse(startDate), LocalDate.parse(endDate));
}

@Test
void givenPendingLoanWithOverApplyDisabled_whenCalculateLoanCollectionData_thenOverAppliedAmountNotSet() {
Loan loan = mock(Loan.class);
LoanProduct loanProduct = mock(LoanProduct.class);
when(loan.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.isAllowApprovedDisbursedAmountsOverApplied()).thenReturn(false);
when(loan.isSubmittedAndPendingApproval()).thenReturn(true);
when(loanRepository.findById(1L)).thenReturn(Optional.of(loan));

CollectionData result = underTest.calculateLoanCollectionData(1L);

assertThat(result).isNotNull();
// over-apply disabled → helper not called → field stays at template default
assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isEqualByComparingTo(BigDecimal.ZERO);
}

@Test
void givenPendingLoanWithOverApplyEnabled_whenCalculateLoanCollectionData_thenOverAppliedAmountIsSet() {
Loan loan = mock(Loan.class);
LoanProduct loanProduct = mock(LoanProduct.class);
when(loan.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.isAllowApprovedDisbursedAmountsOverApplied()).thenReturn(true);
when(loanProduct.getOverAppliedCalculationType()).thenReturn("flat");
when(loanProduct.getOverAppliedNumber()).thenReturn(500);
when(loan.getProposedPrincipal()).thenReturn(BigDecimal.valueOf(10000));
when(loan.getApprovedPrincipal()).thenReturn(BigDecimal.valueOf(10000));
when(loan.getDisbursedAmount()).thenReturn(BigDecimal.ZERO);
LoanProductRelatedDetail detail = mock(LoanProductRelatedDetail.class);
when(detail.isEnableIncomeCapitalization()).thenReturn(false);
when(loan.getLoanRepaymentScheduleDetail()).thenReturn(detail);
when(loan.isSubmittedAndPendingApproval()).thenReturn(true);
when(loanRepository.findById(1L)).thenReturn(Optional.of(loan));

CollectionData result = underTest.calculateLoanCollectionData(1L);

assertThat(result).isNotNull();
// flat over-apply: 10000 + 500 = 10500, minus 0 disbursed = 10500
assertThat(result.getAvailableDisbursementAmountWithOverApplied()).isEqualByComparingTo(BigDecimal.valueOf(10500));
}

}
Loading