FINERACT-2181: Not allow Reschedule loan with interest rate change from / to zero
This commit is contained in:
committed by
Adam Saghy
parent
a591f26546
commit
bf040561ab
+1
@@ -53,6 +53,7 @@ public final class RescheduleLoansApiConstants {
|
|||||||
public static final String rescheduleForMultiDisbursementNotSupportedErrorCode = "loan.reschedule.tranche.multidisbursement.error.code";
|
public static final String rescheduleForMultiDisbursementNotSupportedErrorCode = "loan.reschedule.tranche.multidisbursement.error.code";
|
||||||
public static final String rescheduleMultipleOperationsNotSupportedErrorCode = "loan.reschedule.multioperations.error.code";
|
public static final String rescheduleMultipleOperationsNotSupportedErrorCode = "loan.reschedule.multioperations.error.code";
|
||||||
public static final String rescheduleSelectedOperationNotSupportedErrorCode = "loan.reschedule.selectedoperationnotsupported.error.code";
|
public static final String rescheduleSelectedOperationNotSupportedErrorCode = "loan.reschedule.selectedoperationnotsupported.error.code";
|
||||||
|
public static final String rescheduleNotAllowedFromInterestRateZeroErrorCode = "loan.reschedule.not.allowed.from.current.interest.rate.zero";
|
||||||
public static final String allCommandParamName = "all";
|
public static final String allCommandParamName = "all";
|
||||||
public static final String approveCommandParamName = "approve";
|
public static final String approveCommandParamName = "approve";
|
||||||
public static final String pendingCommandParamName = "pending";
|
public static final String pendingCommandParamName = "pending";
|
||||||
|
|||||||
+10
-3
@@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
|||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
@@ -72,12 +73,17 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq
|
|||||||
@Qualifier("progressiveLoanRescheduleRequestDataValidatorImpl")
|
@Qualifier("progressiveLoanRescheduleRequestDataValidatorImpl")
|
||||||
private final LoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidatorDelegate;
|
private final LoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidatorDelegate;
|
||||||
|
|
||||||
public static BigDecimal validateInterestRate(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
|
public static BigDecimal validateInterestRate(final BigDecimal currentInterestRate, final FromJsonHelper fromJsonHelper,
|
||||||
DataValidatorBuilder dataValidatorBuilder) {
|
final JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) {
|
||||||
final BigDecimal interestRate = fromJsonHelper
|
final BigDecimal interestRate = fromJsonHelper
|
||||||
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
|
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
|
||||||
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull()
|
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull()
|
||||||
.positiveAmount();
|
.positiveAmount();
|
||||||
|
if (interestRate != null && MathUtil.isZero(currentInterestRate) && !MathUtil.isZero(interestRate)) {
|
||||||
|
dataValidatorBuilder.reset().failWithCode(RescheduleLoansApiConstants.newInterestRateParamName,
|
||||||
|
RescheduleLoansApiConstants.rescheduleNotAllowedFromInterestRateZeroErrorCode,
|
||||||
|
"Loan rescheduling is not allowed from interest rate 0 (zero)");
|
||||||
|
}
|
||||||
return interestRate;
|
return interestRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +252,8 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq
|
|||||||
validateLoanIsActive(loan, dataValidatorBuilder);
|
validateLoanIsActive(loan, dataValidatorBuilder);
|
||||||
validateSubmittedOnDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder);
|
validateSubmittedOnDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder);
|
||||||
final LocalDate rescheduleFromDate = validateAndRetrieveRescheduleFromDate(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
final LocalDate rescheduleFromDate = validateAndRetrieveRescheduleFromDate(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||||
validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper, jsonElement,
|
||||||
|
dataValidatorBuilder);
|
||||||
validateGraceOnPrincipal(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
validateGraceOnPrincipal(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||||
validateGraceOnInterest(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
validateGraceOnInterest(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||||
validateExtraTerms(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
validateExtraTerms(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||||
|
|||||||
+2
-1
@@ -78,7 +78,8 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
|
|||||||
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||||
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
validateRescheduleReasonComment(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
||||||
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
|
LocalDate adjustedDueDate = validateAndRetrieveAdjustedDate(fromJsonHelper, jsonElement, rescheduleFromDate, dataValidatorBuilder);
|
||||||
BigDecimal interestRate = validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
|
BigDecimal interestRate = validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper,
|
||||||
|
jsonElement, dataValidatorBuilder);
|
||||||
validateUnsupportedParams(jsonElement, dataValidatorBuilder);
|
validateUnsupportedParams(jsonElement, dataValidatorBuilder);
|
||||||
|
|
||||||
boolean hasInterestRateChange = interestRate != null;
|
boolean hasInterestRateChange = interestRate != null;
|
||||||
|
|||||||
+109
@@ -6000,6 +6000,115 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uc156: Avoid Loan Reschedule to modify Interest Rate from X value to Zero
|
||||||
|
// 1. Create a Loan product
|
||||||
|
// 2. Submit, Approve and Disburse Loan with Nominal Interest equal to 4%
|
||||||
|
// 3. Apply a Loan repayment
|
||||||
|
// 4. Try to create Loan Reschedule with new Interest Rate equal to zero to get the exception
|
||||||
|
@Test
|
||||||
|
public void uc156() {
|
||||||
|
final String operationDate = "1 April 2025";
|
||||||
|
AtomicLong createdLoanId = new AtomicLong();
|
||||||
|
runAt("1 April 2025", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
PostLoanProductsRequest product = create4IProgressive().interestRatePerPeriod(4.0).numberOfRepayments(4)//
|
||||||
|
.installmentAmountInMultiplesOf(null)//
|
||||||
|
.multiDisburseLoan(false)//
|
||||||
|
.disallowExpectedDisbursements(null)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||||
|
.overAppliedCalculationType(null)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.overAppliedNumber(null)//
|
||||||
|
;//
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(),
|
||||||
|
operationDate, 1000.0, 4.0, 4, null);
|
||||||
|
|
||||||
|
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
createdLoanId.set(loanResponse.getLoanId());
|
||||||
|
|
||||||
|
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
|
||||||
|
.approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
|
||||||
|
|
||||||
|
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
|
||||||
|
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1000.0)));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("1 May 2025", () -> {
|
||||||
|
executeInlineCOB(createdLoanId.get());
|
||||||
|
|
||||||
|
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
|
||||||
|
.transactionDate("1 May 2025").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(250.00));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("6 May 2025", () -> {
|
||||||
|
executeInlineCOB(createdLoanId.get());
|
||||||
|
|
||||||
|
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
|
||||||
|
() -> loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
|
||||||
|
.loanId(createdLoanId.get()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("6 May 2025")
|
||||||
|
.newInterestRate(BigDecimal.ZERO).rescheduleReasonId(1L).rescheduleFromDate("1 June 2025")));
|
||||||
|
|
||||||
|
Assertions.assertTrue(
|
||||||
|
callFailedRuntimeException.getMessage().contains("The parameter `newInterestRate` must be greater than 0."));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// uc157: Avoid Loan Reschedule to modify Interest Rate from Zero to X value
|
||||||
|
// 1. Create a Loan product
|
||||||
|
// 2. Submit, Approve and Disburse Loan with Nominal Interest equal to 0 (zero)
|
||||||
|
// 3. Apply a Loan repayment
|
||||||
|
// 4. Try to create Loan Reschedule with new Interest Rate greater than zero to get the exception
|
||||||
|
@Test
|
||||||
|
public void uc157() {
|
||||||
|
final String operationDate = "1 April 2025";
|
||||||
|
AtomicLong createdLoanId = new AtomicLong();
|
||||||
|
runAt("1 April 2025", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
PostLoanProductsRequest product = create4IProgressive().interestRatePerPeriod(0.0).numberOfRepayments(4)//
|
||||||
|
.installmentAmountInMultiplesOf(null)//
|
||||||
|
.multiDisburseLoan(false)//
|
||||||
|
.disallowExpectedDisbursements(null)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false)//
|
||||||
|
.overAppliedCalculationType(null)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.overAppliedNumber(null)//
|
||||||
|
;//
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(),
|
||||||
|
operationDate, 1000.0, 0.0, 4, null);
|
||||||
|
|
||||||
|
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
createdLoanId.set(loanResponse.getLoanId());
|
||||||
|
|
||||||
|
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
|
||||||
|
.approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
|
||||||
|
|
||||||
|
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
|
||||||
|
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1000.0)));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("1 May 2025", () -> {
|
||||||
|
executeInlineCOB(createdLoanId.get());
|
||||||
|
|
||||||
|
loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
|
||||||
|
.transactionDate("1 May 2025").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(250.00));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("6 May 2025", () -> {
|
||||||
|
executeInlineCOB(createdLoanId.get());
|
||||||
|
|
||||||
|
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
|
||||||
|
() -> loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
|
||||||
|
.loanId(createdLoanId.get()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("6 May 2025")
|
||||||
|
.newInterestRate(BigDecimal.valueOf(4.0)).rescheduleReasonId(1L).rescheduleFromDate("1 June 2025")));
|
||||||
|
|
||||||
|
LOG.info("ERROR: {}", callFailedRuntimeException.getMessage());
|
||||||
|
Assertions.assertTrue(
|
||||||
|
callFailedRuntimeException.getMessage().contains("Loan rescheduling is not allowed from interest rate 0 (zero)"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
|
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
|
||||||
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
|
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
|
||||||
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
|
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
|
||||||
|
|||||||
Reference in New Issue
Block a user