FINERACT-1971: Disable delinquency calculation for non active loans

This commit is contained in:
Ruchi Dhamankar
2024-05-09 15:18:04 +02:00
committed by Adam Saghy
parent be655756a5
commit 5d0152c262
8 changed files with 316 additions and 5 deletions
@@ -4186,7 +4186,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return getStatus().isClosedWithOutsandingAmountMarkedForReschedule();
}
private boolean isCancelled() {
public boolean isCancelled() {
return isRejected() || isWithdrawn();
}
@@ -7273,5 +7273,4 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public void updateEnableInstallmentLevelDelinquency(boolean enableInstallmentLevelDelinquency) {
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
}
}
@@ -126,8 +126,8 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
if (optLoan.isPresent()) {
final Loan loan = optLoan.get();
// If the Loan is not Active yet, return template data
if (loan.isSubmittedAndPendingApproval() || loan.isApproved()) {
// If the Loan is not Active yet or is cancelled (rejected or withdrawn), return template data
if (loan.isSubmittedAndPendingApproval() || loan.isApproved() || loan.isCancelled()) {
return CollectionData.template();
}
@@ -137,6 +137,9 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
final String nextPaymentDueDateConfig = configurationDomainService.getNextPaymentDateConfigForLoan();
// Below method calculates delinquency for active loans only and returns template data for Closed or
// Overpaid
// loans
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()));
collectionData.setNextPaymentDueDate(loan.possibleNextRepaymentDate(nextPaymentDueDateConfig));
@@ -162,7 +162,9 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
}
CollectionData collectionData = null;
// If the Loan is not Active yet, return template data
if (loan.isSubmittedAndPendingApproval() || loan.isApproved()) {
// If the Loan is Rejected, Closed written-off, Withdrawn by Client, Closed with outstanding marked for
// reschedule, Closed obligation met, Overpaid, return template data
if (loan.isSubmittedAndPendingApproval() || loan.isApproved() || loan.isClosed() || loan.getStatus().isOverpaid()) {
collectionData = CollectionData.template();
} else {
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
@@ -56,6 +56,14 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
boolean overdueSinceDateWasSet = false;
boolean firstNotYetDueInstallment = false;
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
// If the Loan is not Active yet, return template data
// If the Loan is Rejected, Closed written-off, Withdrawn by Client, Closed with outstanding marked for
// reschedule, Closed obligation met, Overpaid return template data
if (loan.isSubmittedAndPendingApproval() || loan.isApproved() || loan.isClosed() || loan.getStatus().isOverpaid()) {
return CollectionData.template();
}
// Get the oldest overdue installment if exists one
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (!installment.isObligationsMet()) {
@@ -124,6 +132,14 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
boolean overdueSinceDateWasSet = false;
boolean firstNotYetDueInstallment = false;
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
// If the Loan is not Active yet, return template data
// If the Loan is Rejected, Closed written-off, Withdrawn by Client, Closed with outstanding marked for
// reschedule, Closed obligation met, Overpaid, return template data
if (loan.isSubmittedAndPendingApproval() || loan.isApproved() || loan.isClosed() || loan.getStatus().isOverpaid()) {
return new LoanDelinquencyData(collectionData, loanInstallmentsCollectionData);
}
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
CollectionData installmentCollectionData = CollectionData.template();
if (!installment.isObligationsMet()) {
@@ -49,6 +49,7 @@ import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
@@ -123,6 +124,7 @@ public class LoanDelinquencyDomainServiceTest {
when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
when(loan.getCurrency()).thenReturn(currency);
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
CollectionData collectionData = underTest.getOverdueCollectionData(loan, effectiveDelinquencyList);
@@ -150,6 +152,7 @@ public class LoanDelinquencyDomainServiceTest {
when(loan.getLoanTransactions(Mockito.any(Predicate.class))).thenReturn(Collections.emptyList());
when(loan.getLastLoanRepaymentScheduleInstallment()).thenReturn(repaymentScheduleInstallments.get(0));
when(loan.getCurrency()).thenReturn(currency);
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
when(delinquencyEffectivePauseHelper.getPausedDaysBeforeDate(effectiveDelinquencyList, businessDate)).thenReturn(0L);
CollectionData collectionData = underTest.getOverdueCollectionData(loan, effectiveDelinquencyList);
@@ -186,6 +189,7 @@ public class LoanDelinquencyDomainServiceTest {
when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
when(loan.getCurrency()).thenReturn(currency);
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
CollectionData collectionData = underTest.getOverdueCollectionData(loan, effectiveDelinquencyList);
@@ -217,6 +221,7 @@ public class LoanDelinquencyDomainServiceTest {
when(loan.getLastLoanRepaymentScheduleInstallment()).thenReturn(repaymentScheduleInstallments.get(0));
when(loan.getCurrency()).thenReturn(currency);
when(loan.isEnableInstallmentLevelDelinquency()).thenReturn(true);
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
when(delinquencyEffectivePauseHelper.getPausedDaysBeforeDate(effectiveDelinquencyList, businessDate)).thenReturn(0L);
LoanDelinquencyData collectionData = underTest.getLoanDelinquencyData(loan, effectiveDelinquencyList);
@@ -266,6 +271,7 @@ public class LoanDelinquencyDomainServiceTest {
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
when(loan.isEnableInstallmentLevelDelinquency()).thenReturn(true);
when(loan.getCurrency()).thenReturn(currency);
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
when(loan.getLoanTransactions(Mockito.any(Predicate.class))).thenReturn(Arrays.asList(loanTransaction));
when(delinquencyEffectivePauseHelper.getPausedDaysBeforeDate(effectiveDelinquencyList, businessDate)).thenReturn(0L);
@@ -851,6 +851,15 @@ public abstract class BaseLoanIntegrationTest {
assertEquals(loanStatus.getCode(), loanDetails.getStatus().getCode());
}
protected void undoLoanApproval(Long loanId) {
loanTransactionHelper.undoApprovalForLoan(loanId, new PostLoansLoanIdRequest());
}
protected void rejectLoan(Long loanId, String rejectedOnDate) {
loanTransactionHelper.rejectLoan(loanId,
new PostLoansLoanIdRequest().rejectedOnDate(rejectedOnDate).locale("en").dateFormat(DATETIME_PATTERN));
}
@RequiredArgsConstructor
public static class BatchRequestBuilder {
@@ -0,0 +1,272 @@
/**
* 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.integrationtests;
import static org.assertj.core.api.Assertions.assertThat;
import java.math.BigDecimal;
import java.util.List;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.client.models.GetLoansLoanIdLoanInstallmentLevelDelinquency;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class LoanDelinquencyForNonActiveAccountsTest extends BaseLoanIntegrationTest {
private SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(this.requestSpec);
@Test
public void testDelinquencyCalculationsForRejectedLoanAccount() {
runAt("06 May 2024", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create DelinquencyBuckets
Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(//
Pair.of(1, 10), //
Pair.of(11, 30), //
Pair.of(31, 60), //
Pair.of(61, null)//
));
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
loanProductsRequest.setDelinquencyBucketId(delinquencyBucketId.longValue());
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "06 May 2024", 1000.0, 4);
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
// Update Business Date
updateBusinessDate("17 June 2024");
// Undo Approval
undoLoanApproval(loanId);
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
// Reject Loan
rejectLoan(loanId, "17 June 2024");
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
});
}
@Test
public void testDelinquencyCalculationsForRejectedLoanAccountCOBTest() {
runAt("06 May 2024", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create DelinquencyBuckets
Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(//
Pair.of(1, 10), //
Pair.of(11, 30), //
Pair.of(31, 60), //
Pair.of(61, null)//
));
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
loanProductsRequest.setDelinquencyBucketId(delinquencyBucketId.longValue());
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "06 May 2024", 1000.0, 4);
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
// Update Business Date
updateBusinessDate("17 June 2024");
// Undo Approval
undoLoanApproval(loanId);
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
// Reject Loan
rejectLoan(loanId, "17 June 2024");
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
// Update Business Date
updateBusinessDate("18 June 2024");
// execute COB
schedulerJobHelper.executeAndAwaitJob("Loan COB");
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
});
}
@Test
public void testDelinquencyCalculationsForClosedLoanAccount() {
runAt("06 May 2024", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create DelinquencyBuckets
Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(//
Pair.of(1, 10), //
Pair.of(11, 30), //
Pair.of(31, 60), //
Pair.of(61, null)//
));
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
loanProductsRequest.setDelinquencyBucketId(delinquencyBucketId.longValue());
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "06 May 2024", 1000.0, 4);
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
// Update Business Date
updateBusinessDate("17 June 2024");
// disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1000), "06 May 2024");
verifyDelinquency(loanId, 12, "250.0", null, null, //
delinquency(11, 30, "250.0"));
// re-pay Loan
addRepaymentForLoan(loanId, 1000.0, "17 June 2024");
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", "17 June 2024", "1000.0");
});
}
@Test
public void testDelinquencyCalculationsForOverPaidLoanAccount() {
runAt("06 May 2024", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
// Create DelinquencyBuckets
Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(//
Pair.of(1, 10), //
Pair.of(11, 30), //
Pair.of(31, 60), //
Pair.of(61, null)//
));
// Create Loan Product
PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
loanProductsRequest.setDelinquencyBucketId(delinquencyBucketId.longValue());
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
// Apply and Approve Loan
Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "06 May 2024", 1000.0, 4);
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", null, null);
// Update Business Date
updateBusinessDate("17 June 2024");
// disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1000), "06 May 2024");
verifyDelinquency(loanId, 12, "250.0", null, null, //
delinquency(11, 30, "250.0"));
// over-pay Loan
addRepaymentForLoan(loanId, 1200.0, "17 June 2024");
// Delinquency Calculations
verifyDelinquency(loanId, 0, "0.0", "17 June 2024", "1200.0");
});
}
private void verifyDelinquency(Long loanId, Integer loanLevelDelinquentDays, String loanLevelDelinquentAmount,
String expectedLastRepaymentDate, String expectedLastRepaymentAmount,
InstallmentLevelDelinquencyAPIIntegrationTests.DelinquencyData... expectedInstallmentLevelDelinquencyData) {
GetLoansLoanIdResponse loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
assertThat(loan.getDelinquent()).isNotNull();
List<GetLoansLoanIdLoanInstallmentLevelDelinquency> installmentLevelDelinquency = loan.getDelinquent()
.getInstallmentLevelDelinquency();
assertThat(loan.getDelinquent().getDelinquentDays()).isEqualTo(loanLevelDelinquentDays);
assertThat(loan.getDelinquent().getDelinquentAmount()).isEqualByComparingTo(Double.valueOf(loanLevelDelinquentAmount));
if (expectedLastRepaymentDate != null && expectedLastRepaymentAmount != null) {
assertThat(loan.getDelinquent().getLastRepaymentDate()).isNotNull();
Assertions.assertEquals(expectedLastRepaymentDate, loan.getDelinquent().getLastRepaymentDate().format(dateTimeFormatter));
assertThat(loan.getDelinquent().getLastRepaymentAmount()).isNotNull();
assertThat(loan.getDelinquent().getLastRepaymentAmount()).isEqualByComparingTo(Double.valueOf(expectedLastRepaymentAmount));
}
if (expectedInstallmentLevelDelinquencyData != null && expectedInstallmentLevelDelinquencyData.length > 0) {
assertThat(installmentLevelDelinquency).isNotNull();
assertThat(installmentLevelDelinquency).hasSize(expectedInstallmentLevelDelinquencyData.length);
for (int i = 0; i < expectedInstallmentLevelDelinquencyData.length; i++) {
assertThat(installmentLevelDelinquency.get(i).getMaximumAgeDays())
.isEqualTo(expectedInstallmentLevelDelinquencyData[i].maxAgeDays);
assertThat(installmentLevelDelinquency.get(i).getMinimumAgeDays())
.isEqualTo(expectedInstallmentLevelDelinquencyData[i].minAgeDays);
assertThat(installmentLevelDelinquency.get(i).getDelinquentAmount())
.isEqualByComparingTo(expectedInstallmentLevelDelinquencyData[i].delinquentAmount);
}
} else {
assertThat(installmentLevelDelinquency).isNull();
}
}
@AllArgsConstructor
public static class DelinquencyData {
Integer minAgeDays;
Integer maxAgeDays;
BigDecimal delinquentAmount;
}
private static InstallmentLevelDelinquencyAPIIntegrationTests.DelinquencyData delinquency(Integer minAgeDays, Integer maxAgeDays,
String delinquentAmount) {
return new InstallmentLevelDelinquencyAPIIntegrationTests.DelinquencyData(minAgeDays, maxAgeDays, new BigDecimal(delinquentAmount));
}
}
@@ -2015,4 +2015,8 @@ public class LoanTransactionHelper extends IntegrationTest {
return Utils.performServerPut(requestSpec, responseSpec, UPDATE_LOAN_PRODUCT_URL, request, null);
}
public PostLoansLoanIdResponse undoApprovalForLoan(Long loanId, PostLoansLoanIdRequest request) {
return ok(fineract().loans.stateTransitions(loanId, request, "undoapproval"));
}
}