Skip to content
Merged
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 @@ -22,7 +22,9 @@ public enum LoanRescheduleErrorMessage {

LOAN_CHARGED_OFF("Loan: %s reschedule installment is not allowed. Loan Account is Charged-off"), //
LOAN_RESCHEDULE_DATE_NOT_IN_FUTURE("Loan Reschedule From date (%s) for Loan: %s should be in the future."), //
LOAN_LOCKED_BY_COB("Loan is locked by the COB job. Loan ID: %s");//
LOAN_LOCKED_BY_COB("Loan is locked by the COB job. Loan ID: %s"), //
LOAN_RESCHEDULE_NOT_ALLOWED_FROM_ZERO_TO_NEW_INTEREST_RATE("Failed data validation due to: newInterestRate."), //
LOAN_RESCHEDULE_NOT_ALLOWED_FROM_CURRENT_INTEREST_RATE_TO_ZERO("The parameter `newInterestRate` must be greater than 0.");//

private final String messageTemplate;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ public void createLoanRescheduleError(int errorCodeExpected, String errorMessage
String rescheduleFromDateFormatted = localDate.format(FORMATTER_HU);
String errorMessageExpected = "";
int expectedParameterCount = loanRescheduleErrorMessage.getExpectedParameterCount();
if (expectedParameterCount == 1) {
if (expectedParameterCount == 0) {
errorMessageExpected = loanRescheduleErrorMessage.getMessageTemplate();
} else if (expectedParameterCount == 1) {
errorMessageExpected = loanRescheduleErrorMessage.getValue(loanId);
} else if (expectedParameterCount == 2) {
errorMessageExpected = loanRescheduleErrorMessage.getValue(rescheduleFromDateFormatted, loanId);
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private RescheduleLoansApiConstants() {
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 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 approveCommandParamName = "approve";
public static final String pendingCommandParamName = "pending";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
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.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
Expand Down Expand Up @@ -72,12 +73,17 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq
@Qualifier("progressiveLoanRescheduleRequestDataValidatorImpl")
private final LoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidatorDelegate;

public static BigDecimal validateInterestRate(FromJsonHelper fromJsonHelper, JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder) {
public static BigDecimal validateInterestRate(final BigDecimal currentInterestRate, final FromJsonHelper fromJsonHelper,
final JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) {
final BigDecimal interestRate = fromJsonHelper
.extractBigDecimalWithLocaleNamed(RescheduleLoansApiConstants.newInterestRateParamName, jsonElement);
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.newInterestRateParamName).value(interestRate).ignoreIfNull()
.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;
}

Expand Down Expand Up @@ -246,7 +252,8 @@ public void validateForCreateAction(final JsonCommand jsonCommand, final Loan lo
validateLoanIsActive(loan, dataValidatorBuilder);
validateSubmittedOnDate(fromJsonHelper, loan, jsonElement, dataValidatorBuilder);
final LocalDate rescheduleFromDate = validateAndRetrieveRescheduleFromDate(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateInterestRate(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateInterestRate(loan.getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate(), fromJsonHelper, jsonElement,
dataValidatorBuilder);
validateGraceOnPrincipal(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateGraceOnInterest(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateExtraTerms(fromJsonHelper, jsonElement, dataValidatorBuilder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ public void validateForCreateAction(JsonCommand jsonCommand, Loan loan) {
validateRescheduleReasonId(fromJsonHelper, jsonElement, dataValidatorBuilder);
validateRescheduleReasonComment(fromJsonHelper, jsonElement, 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);

boolean hasInterestRateChange = interestRate != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6000,6 +6000,115 @@ public void uc155() {
});
}

// 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,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
Expand Down
Loading