FINERACT-2081: fix NPE Loan product creation with null values in Interest rate or Number of repayments

This commit is contained in:
Jose Alberto Hernandez
2024-08-28 15:38:11 +02:00
committed by Adam Saghy
parent 6ca02686d2
commit 284022abba
2 changed files with 77 additions and 27 deletions
@@ -43,6 +43,7 @@ import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRu
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; 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.MathUtil;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
@@ -569,7 +570,7 @@ public final class LoanProductDataValidator {
Locale.getDefault()); Locale.getDefault());
baseDataValidator.reset().parameter(INTEREST_RATE_FREQUENCY_TYPE).value(interestRateFrequencyType).notNull().inMinMaxRange(0, baseDataValidator.reset().parameter(INTEREST_RATE_FREQUENCY_TYPE).value(interestRateFrequencyType).notNull().inMinMaxRange(0,
4); 4);
isInterestBearing = interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; isInterestBearing = MathUtil.isGreaterThanZero(interestRatePerPeriod);
} }
// Fixed Length validation // Fixed Length validation
@@ -1296,11 +1297,14 @@ public final class LoanProductDataValidator {
.integerGreaterThanZero(); .integerGreaterThanZero();
} }
Integer numberOfRepayments = loanProduct.getNumberOfRepayments(); Integer numberOfRepayments = null;
if (this.fromApiJsonHelper.parameterExists(NUMBER_OF_REPAYMENTS, element)) { if (this.fromApiJsonHelper.parameterExists(NUMBER_OF_REPAYMENTS, element)) {
numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(NUMBER_OF_REPAYMENTS, element); numberOfRepayments = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(NUMBER_OF_REPAYMENTS, element);
baseDataValidator.reset().parameter(NUMBER_OF_REPAYMENTS).value(numberOfRepayments).notNull().integerGreaterThanZero(); baseDataValidator.reset().parameter(NUMBER_OF_REPAYMENTS).value(numberOfRepayments).notNull().integerGreaterThanZero();
} }
if (numberOfRepayments == null) {
numberOfRepayments = loanProduct.getNumberOfRepayments();
}
Integer repaymentEvery = loanProduct.getLoanProductRelatedDetail().getRepayEvery(); Integer repaymentEvery = loanProduct.getLoanProductRelatedDetail().getRepayEvery();
if (this.fromApiJsonHelper.parameterExists(REPAYMENT_EVERY, element)) { if (this.fromApiJsonHelper.parameterExists(REPAYMENT_EVERY, element)) {
@@ -1608,7 +1612,7 @@ public final class LoanProductDataValidator {
} }
baseDataValidator.reset().parameter(INTEREST_RATE_FREQUENCY_TYPE).value(interestRateFrequencyType).notNull().inMinMaxRange(0, baseDataValidator.reset().parameter(INTEREST_RATE_FREQUENCY_TYPE).value(interestRateFrequencyType).notNull().inMinMaxRange(0,
4); 4);
isInterestBearing = interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; isInterestBearing = MathUtil.isGreaterThanZero(interestRatePerPeriod);
} }
// Fixed Length validation // Fixed Length validation
@@ -2541,34 +2545,36 @@ public final class LoanProductDataValidator {
public void validateRepaymentPeriodWithGraceSettings(final Integer numberOfRepayments, final Integer graceOnPrincipalPayment, public void validateRepaymentPeriodWithGraceSettings(final Integer numberOfRepayments, final Integer graceOnPrincipalPayment,
final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final Integer recurringMoratoriumOnPrincipalPeriods, final Integer graceOnInterestPayment, final Integer graceOnInterestCharged, final Integer recurringMoratoriumOnPrincipalPeriods,
DataValidatorBuilder baseDataValidator) { DataValidatorBuilder baseDataValidator) {
if (numberOfRepayments <= defaultToZeroIfNull(graceOnPrincipalPayment)) { if (numberOfRepayments != null) {
baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment) if (numberOfRepayments <= defaultToZeroIfNull(graceOnPrincipalPayment)) {
.failWithCode(".mustBeLessThan.numberOfRepayments"); baseDataValidator.reset().parameter("graceOnPrincipalPayment").value(graceOnPrincipalPayment)
} .failWithCode(".mustBeLessThan.numberOfRepayments");
}
if (numberOfRepayments <= defaultToZeroIfNull(graceOnInterestPayment)) { if (numberOfRepayments <= defaultToZeroIfNull(graceOnInterestPayment)) {
baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment) baseDataValidator.reset().parameter("graceOnInterestPayment").value(graceOnInterestPayment)
.failWithCode(".mustBeLessThan.numberOfRepayments"); .failWithCode(".mustBeLessThan.numberOfRepayments");
} }
if (numberOfRepayments < defaultToZeroIfNull(graceOnInterestCharged)) { if (numberOfRepayments < defaultToZeroIfNull(graceOnInterestCharged)) {
baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged) baseDataValidator.reset().parameter("graceOnInterestCharged").value(graceOnInterestCharged)
.failWithCode(".mustBeLessThan.numberOfRepayments"); .failWithCode(".mustBeLessThan.numberOfRepayments");
} }
int graceOnPrincipal = 0; int graceOnPrincipal = 0;
if (graceOnPrincipalPayment != null) { if (graceOnPrincipalPayment != null) {
graceOnPrincipal = graceOnPrincipalPayment; graceOnPrincipal = graceOnPrincipalPayment;
} }
int recurMoratoriumOnPrincipal = 0; int recurMoratoriumOnPrincipal = 0;
if (recurringMoratoriumOnPrincipalPeriods != null) { if (recurringMoratoriumOnPrincipalPeriods != null) {
recurMoratoriumOnPrincipal = recurringMoratoriumOnPrincipalPeriods; recurMoratoriumOnPrincipal = recurringMoratoriumOnPrincipalPeriods;
} }
if ((recurMoratoriumOnPrincipal > 0) && ((numberOfRepayments - graceOnPrincipal) % (recurMoratoriumOnPrincipal + 1) != 1)) { if ((recurMoratoriumOnPrincipal > 0) && ((numberOfRepayments - graceOnPrincipal) % (recurMoratoriumOnPrincipal + 1) != 1)) {
baseDataValidator.reset().parameter("graceOnPrincipalPayments.and.recurringMoratoriumOnPrincipalPeriods") baseDataValidator.reset().parameter("graceOnPrincipalPayments.and.recurringMoratoriumOnPrincipalPeriods")
.value(graceOnPrincipal).value(recurMoratoriumOnPrincipal) .value(graceOnPrincipal).value(recurMoratoriumOnPrincipal)
.failWithCode("causes.principal.moratorium.for.last.installment"); .failWithCode("causes.principal.moratorium.for.last.installment");
}
} }
} }
@@ -364,6 +364,50 @@ public class LoanProductWithAdvancedPaymentAllocationIntegrationTests {
loanProductError.get(0).get("userMessageGlobalisationCode")); loanProductError.get(0).get("userMessageGlobalisationCode"));
} }
@Test
public void testCreateShouldFailWhenNoNumberOfRepaymentsIsProvided() {
// given
ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(400).build();
LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(REQUEST_SPEC, errorResponse);
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation();
// when
String loanProduct = loanProductTestBuilder(customization -> customization
.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation).withPrincipal("15,000.00")
.withNumberOfRepayments(null).withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1")
.withAccountingRulePeriodicAccrual(new Account[] { ASSET_ACCOUNT, EXPENSE_ACCOUNT, INCOME_ACCOUNT, OVERPAYMENT_ACCOUNT })
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
.withFeeAndPenaltyAssetAccount(FEE_PENALTY_ACCOUNT).build());
// when
List<Map<String, String>> loanProductError = validationErrorHelper.getLoanProductError(loanProduct, "errors");
Assertions.assertEquals("The parameter numberOfRepayments is mandatory.",
loanProductError.get(0).get("defaultUserMessage").replace('`', ' '));
}
@Test
public void testCreateShouldFailWhenNoInterestRateIsProvided() {
// given
ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(400).build();
LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(REQUEST_SPEC, errorResponse);
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation();
AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation();
// when
String loanProduct = loanProductTestBuilder(customization -> customization
.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation).withPrincipal("15,000.00")
.withNumberOfRepayments("4").withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod(null)
.withAccountingRulePeriodicAccrual(new Account[] { ASSET_ACCOUNT, EXPENSE_ACCOUNT, INCOME_ACCOUNT, OVERPAYMENT_ACCOUNT })
.withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance()
.withFeeAndPenaltyAssetAccount(FEE_PENALTY_ACCOUNT).build());
// when
List<Map<String, String>> loanProductError = validationErrorHelper.getLoanProductError(loanProduct, "errors");
Assertions.assertEquals("The parameter interestRatePerPeriod is mandatory.",
loanProductError.get(0).get("defaultUserMessage").replace('`', ' '));
}
private String loanProductTestBuilder(Consumer<LoanProductTestBuilder> customization) { private String loanProductTestBuilder(Consumer<LoanProductTestBuilder> customization) {
LoanProductTestBuilder builder = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4") LoanProductTestBuilder builder = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4")
.withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1") .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1")