Allow multi-disbursements without setting up expected tranche details (#2129)

This commit is contained in:
John-Woodlock
2022-03-10 10:37:35 +01:00
committed by GitHub
parent 91a8c948d1
commit a25d07df20
6 changed files with 330 additions and 4 deletions
@@ -109,7 +109,6 @@ public interface LoanProductConstants {
BigDecimal DEFAULT_PRINCIPAL_THRESHOLD_FOR_SINGLE_DISBURSE_LOAN = BigDecimal.valueOf(0);
// Fixed installment configuration related
String canDefineEmiAmountParamName = "canDefineInstallmentAmount";
String installmentAmountInMultiplesOfParamName = "installmentAmountInMultiplesOf";
String fixedPrincipalPercentagePerInstallmentParamName = "fixedPrincipalPercentagePerInstallment";
// Loan Configurable Attributes
@@ -136,4 +135,11 @@ public interface LoanProductConstants {
String RATES_PARAM_NAME = "rates";
// Multiple disbursement related
String installmentAmountInMultiplesOfParamName = "installmentAmountInMultiplesOf";
String DISALLOW_EXPECTED_DISBURSEMENTS = "disallowExpectedDisbursements";
String ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED = "allowApprovedDisbursedAmountsOverApplied";
String OVER_APPLIED_CALCULATION_TYPE = "overAppliedCalculationType";
String OVER_APPLIED_NUMBER = "overAppliedNumber";
}
@@ -24,6 +24,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
@@ -64,6 +65,7 @@ import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate;
import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException;
import org.apache.fineract.portfolio.rate.domain.Rate;
/**
@@ -361,6 +363,20 @@ public class LoanProduct extends AbstractPersistableCustom {
BigDecimal fixedPrincipalPercentagePerInstallment = command
.bigDecimalValueOfParameterNamed(LoanProductConstants.fixedPrincipalPercentagePerInstallmentParamName);
final boolean disallowExpectedDisbursements = command.parameterExists(LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS)
? command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS)
: false;
final boolean allowApprovedDisbursedAmountsOverApplied = command
.parameterExists(LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED)
? command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED)
: false;
final String overAppliedCalculationType = command
.stringValueOfParameterNamedAllowingNull(LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE);
final Integer overAppliedNumber = command.integerValueOfParameterNamed(LoanProductConstants.OVER_APPLIED_NUMBER);
return new LoanProduct(fund, loanTransactionProcessingStrategy, name, shortName, description, currency, principal, minPrincipal,
maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType,
annualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, repaymentEvery,
@@ -375,7 +391,8 @@ public class LoanProduct extends AbstractPersistableCustom {
floatingRate, interestRateDifferential, minDifferentialLendingRate, maxDifferentialLendingRate,
defaultDifferentialLendingRate, isFloatingInterestRateCalculationAllowed, isVariableInstallmentsAllowed,
minimumGapBetweenInstallments, maximumGapBetweenInstallments, syncExpectedWithDisbursementDate, canUseForTopup,
isEqualAmortization, productRates, fixedPrincipalPercentagePerInstallment);
isEqualAmortization, productRates, fixedPrincipalPercentagePerInstallment, disallowExpectedDisbursements,
allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber);
}
@@ -610,7 +627,9 @@ public class LoanProduct extends AbstractPersistableCustom {
Boolean isFloatingInterestRateCalculationAllowed, final Boolean isVariableInstallmentsAllowed,
final Integer minimumGapBetweenInstallments, final Integer maximumGapBetweenInstallments,
final boolean syncExpectedWithDisbursementDate, final boolean canUseForTopup, final boolean isEqualAmortization,
final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment) {
final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean disallowExpectedDisbursements,
final boolean allowApprovedDisbursedAmountsOverApplied, final String overAppliedCalculationType,
final Integer overAppliedNumber) {
this.fund = fund;
this.transactionProcessingStrategy = transactionProcessingStrategy;
this.name = name.trim();
@@ -689,9 +708,68 @@ public class LoanProduct extends AbstractPersistableCustom {
this.canUseForTopup = canUseForTopup;
this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment;
this.disallowExpectedDisbursements = disallowExpectedDisbursements;
this.allowApprovedDisbursedAmountsOverApplied = allowApprovedDisbursedAmountsOverApplied;
this.overAppliedCalculationType = overAppliedCalculationType;
this.overAppliedNumber = overAppliedNumber;
if (rates != null) {
this.rates = rates;
}
validateLoanProductPreSave();
}
public void validateLoanProductPreSave() {
if (this.disallowExpectedDisbursements) {
if (!this.isMultiDisburseLoan()) {
throw new LoanProductGeneralRuleException("allowMultipleDisbursals.not.set.disallowExpectedDisbursements.cant.be.set",
"Allow Multiple Disbursals Not Set - Disallow Expected Disbursals Can't Be Set");
}
}
if (this.allowApprovedDisbursedAmountsOverApplied) {
if (!this.disallowExpectedDisbursements) {
throw new LoanProductGeneralRuleException(
"disallowExpectedDisbursements.not.set.allowApprovedDisbursedAmountsOverApplied.cant.be.set",
"Disallow Expected Disbursals Not Set - Allow Approved / Disbursed Amounts Over Applied Can't Be Set");
}
}
if (this.overAppliedCalculationType == null || this.overAppliedCalculationType.isEmpty()) {
if (this.allowApprovedDisbursedAmountsOverApplied) {
throw new LoanProductGeneralRuleException(
"allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedCalculationType.is.mandatory",
"Allow Approved / Disbursed Amounts Over Applied is Set - Over Applied Calculation Type is Mandatory");
}
} else {
if (!this.allowApprovedDisbursedAmountsOverApplied) {
throw new LoanProductGeneralRuleException(
"allowApprovedDisbursedAmountsOverApplied.is.not.set.overAppliedCalculationType.cant.be.entered",
"Allow Approved / Disbursed Amounts Over Applied is Not Set - Over Applied Calculation Type Can't Be Entered");
}
List<String> overAppliedCalculationTypeAllowedValues = Arrays.asList("percentage", "flat");
if (!overAppliedCalculationTypeAllowedValues.contains(this.overAppliedCalculationType)) {
throw new LoanProductGeneralRuleException("overAppliedCalculationType.must.be.percentage.or.flat",
"Over Applied Calculation Type Must Be 'percentage' or 'flat'");
}
}
if (this.overAppliedNumber != null) {
if (!this.allowApprovedDisbursedAmountsOverApplied) {
throw new LoanProductGeneralRuleException(
"allowApprovedDisbursedAmountsOverApplied.is.not.set.overAppliedNumber.cant.be.entered",
"Allow Approved / Disbursed Amounts Over Applied is Not Set - Over Applied Number Can't Be Entered");
}
} else {
if (this.allowApprovedDisbursedAmountsOverApplied) {
throw new LoanProductGeneralRuleException("allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedNumber.is.mandatory",
"Allow Approved / Disbursed Amounts Over Applied is Set - Over Applied Number is Mandatory");
}
}
}
public MonetaryCurrency getCurrency() {
@@ -1116,6 +1194,34 @@ public class LoanProduct extends AbstractPersistableCustom {
this.fixedPrincipalPercentagePerInstallment = newValue;
}
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS,
this.disallowExpectedDisbursements)) {
final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS);
actualChanges.put(LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS, newValue);
this.disallowExpectedDisbursements = newValue;
}
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED,
this.allowApprovedDisbursedAmountsOverApplied)) {
final boolean newValue = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED);
actualChanges.put(LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED, newValue);
this.allowApprovedDisbursedAmountsOverApplied = newValue;
}
if (command.isChangeInStringParameterNamed(LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE, this.overAppliedCalculationType)) {
final String newValue = command.stringValueOfParameterNamed(LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE);
actualChanges.put(LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE, newValue);
this.overAppliedCalculationType = newValue;
}
if (command.isChangeInIntegerParameterNamed(LoanProductConstants.OVER_APPLIED_NUMBER, this.overAppliedNumber)) {
final Integer newValue = command.integerValueOfParameterNamed(LoanProductConstants.OVER_APPLIED_NUMBER);
actualChanges.put(LoanProductConstants.OVER_APPLIED_NUMBER, newValue);
actualChanges.put("locale", localeAsInput);
this.overAppliedNumber = newValue;
}
return actualChanges;
}
@@ -1481,4 +1587,25 @@ public class LoanProduct extends AbstractPersistableCustom {
public Integer getOverAppliedNumber() {
return overAppliedNumber;
}
public void setDisallowExpectedDisbursements(boolean disallowExpectedDisbursements) {
this.disallowExpectedDisbursements = disallowExpectedDisbursements;
}
public void setAllowApprovedDisbursedAmountsOverApplied(boolean allowApprovedDisbursedAmountsOverApplied) {
this.allowApprovedDisbursedAmountsOverApplied = allowApprovedDisbursedAmountsOverApplied;
}
public void setOverAppliedCalculationType(String overAppliedCalculationType) {
this.overAppliedCalculationType = overAppliedCalculationType;
}
public void setOverAppliedNumber(Integer overAppliedNumber) {
this.overAppliedNumber = overAppliedNumber;
}
public void setLoanProducTrancheDetails(LoanProductTrancheDetails loanProducTrancheDetails) {
this.loanProducTrancheDetails = loanProducTrancheDetails;
}
}
@@ -0,0 +1,31 @@
/**
* 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.portfolio.loanproduct.exception;
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
/**
* {@link AbstractPlatformDomainRuleException} thrown when lending strategy mismatch occurs
*/
public class LoanProductGeneralRuleException extends AbstractPlatformDomainRuleException {
public LoanProductGeneralRuleException(String errorMsg, String errorMsgEnglish) {
super("error.msg." + errorMsg, errorMsgEnglish);
}
}
@@ -109,7 +109,9 @@ public final class LoanProductDataValidator {
LoanProductConstants.recalculationRestFrequencyOnDayParamName,
LoanProductConstants.isCompoundingToBePostedAsTransactionParamName, LoanProductConstants.allowCompoundingOnEodParamName,
LoanProductConstants.CAN_USE_FOR_TOPUP, LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME,
LoanProductConstants.fixedPrincipalPercentagePerInstallmentParamName));
LoanProductConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS,
LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED, LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE,
LoanProductConstants.OVER_APPLIED_NUMBER));
private static final String[] supportedloanConfigurableAttributes = { LoanProductConstants.amortizationTypeParamName,
LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyIdParamName,
@@ -753,6 +755,7 @@ public final class LoanProductDataValidator {
baseDataValidator.reset().parameter("interestType").value(interestType).ignoreIfNull()
.integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue());
}
}
private void validateInterestRecalculationParams(final JsonElement element, final DataValidatorBuilder baseDataValidator,
@@ -252,6 +252,7 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
}
if (!changes.isEmpty()) {
product.validateLoanProductPreSave();
this.loanProductRepository.saveAndFlush(product);
}
@@ -0,0 +1,158 @@
/**
* 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.portfolio.loanproduct.domain;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThrows;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class LoanProductTest {
@Test
public void shouldThrowExceptionWhenAllowMultipleDisbursalsFalseAndDisallowExpectedDisbursementsIsTrue() {
LoanProduct lp = stubLoanProduct(false);
lp.setDisallowExpectedDisbursements(true);
LoanProductGeneralRuleException expectedEx = assertThrows(LoanProductGeneralRuleException.class,
() -> lp.validateLoanProductPreSave());
assertThat(expectedEx.getGlobalisationMessageCode(),
is("error.msg.allowMultipleDisbursals.not.set.disallowExpectedDisbursements.cant.be.set"));
}
@Test
public void shouldThrowExceptionWhenDisallowExpectedDisbursementIsFalseAndAllowApprovedDisbursedAmountsOverAppliedIsTrue() {
LoanProduct lp = stubLoanProduct(true);
lp.setDisallowExpectedDisbursements(false);
lp.setAllowApprovedDisbursedAmountsOverApplied(true);
LoanProductGeneralRuleException expectedEx = assertThrows(LoanProductGeneralRuleException.class,
() -> lp.validateLoanProductPreSave());
assertThat(expectedEx.getGlobalisationMessageCode(),
is("error.msg.disallowExpectedDisbursements.not.set.allowApprovedDisbursedAmountsOverApplied.cant.be.set"));
}
@Test
public void shouldThrowExceptionWhenAllowApprovedDisbursedAmountsOverAppliedIsTrueAndAppliedCalculationTypeIsNull() {
LoanProduct lp = stubLoanProduct(true);
lp.setDisallowExpectedDisbursements(true);
lp.setAllowApprovedDisbursedAmountsOverApplied(true);
lp.setOverAppliedCalculationType(null);
LoanProductGeneralRuleException expectedEx = assertThrows(LoanProductGeneralRuleException.class,
() -> lp.validateLoanProductPreSave());
assertThat(expectedEx.getGlobalisationMessageCode(),
is("error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedCalculationType.is.mandatory"));
}
@Test
public void shouldThrowExceptionWhenAllowApprovedDisbursedAmountsOverAppliedIsTrueAndAppliedCalculationTypeIsEmpty() {
LoanProduct lp = stubLoanProduct(true);
lp.setDisallowExpectedDisbursements(true);
lp.setAllowApprovedDisbursedAmountsOverApplied(true);
lp.setOverAppliedCalculationType("");
LoanProductGeneralRuleException expectedEx = assertThrows(LoanProductGeneralRuleException.class,
() -> lp.validateLoanProductPreSave());
assertThat(expectedEx.getGlobalisationMessageCode(),
is("error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedCalculationType.is.mandatory"));
}
@Test
public void shouldThrowExceptionWhenAllowApprovedDisbursedAmountsOverAppliedIsFalseAndAppliedCalculationTypeIsNotEmpty() {
LoanProduct lp = stubLoanProduct(true);
lp.setDisallowExpectedDisbursements(true);
lp.setAllowApprovedDisbursedAmountsOverApplied(false);
lp.setOverAppliedCalculationType("flat");
LoanProductGeneralRuleException expectedEx = assertThrows(LoanProductGeneralRuleException.class,
() -> lp.validateLoanProductPreSave());
assertThat(expectedEx.getGlobalisationMessageCode(),
is("error.msg.allowApprovedDisbursedAmountsOverApplied.is.not.set.overAppliedCalculationType.cant.be.entered"));
}
@Test
public void shouldThrowExceptionWhenOverAppliedCalculationTypeNotValid() {
LoanProduct lp = stubLoanProduct(true);
lp.setDisallowExpectedDisbursements(true);
lp.setAllowApprovedDisbursedAmountsOverApplied(true);
lp.setOverAppliedCalculationType("notflat");
LoanProductGeneralRuleException expectedEx = assertThrows(LoanProductGeneralRuleException.class,
() -> lp.validateLoanProductPreSave());
assertThat(expectedEx.getGlobalisationMessageCode(), is("error.msg.overAppliedCalculationType.must.be.percentage.or.flat"));
}
@Test
public void shouldThrowExceptionWhenAllowApprovedDisbursedAmountsOverAppliedIsTrueAndAppliedCalculationNumberIsNull() {
LoanProduct lp = stubLoanProduct(true);
lp.setDisallowExpectedDisbursements(true);
lp.setAllowApprovedDisbursedAmountsOverApplied(true);
lp.setOverAppliedCalculationType("flat");
lp.setOverAppliedNumber(null);
LoanProductGeneralRuleException expectedEx = assertThrows(LoanProductGeneralRuleException.class,
() -> lp.validateLoanProductPreSave());
assertThat(expectedEx.getGlobalisationMessageCode(),
is("error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedNumber.is.mandatory"));
}
@Test
public void shouldThrowExceptionWhenAllowApprovedDisbursedAmountsOverAppliedIsFalseAndAppliedCalculationNumberIsNotNull() {
LoanProduct lp = stubLoanProduct(true);
lp.setDisallowExpectedDisbursements(true);
lp.setAllowApprovedDisbursedAmountsOverApplied(false);
lp.setOverAppliedNumber(80);
LoanProductGeneralRuleException expectedEx = assertThrows(LoanProductGeneralRuleException.class,
() -> lp.validateLoanProductPreSave());
assertThat(expectedEx.getGlobalisationMessageCode(),
is("error.msg.allowApprovedDisbursedAmountsOverApplied.is.not.set.overAppliedNumber.cant.be.entered"));
}
private LoanProduct stubLoanProduct(boolean allowMultipleDisbursals) {
LoanProduct lp = new LoanProduct();
lp.setLoanProducTrancheDetails(new LoanProductTrancheDetails(allowMultipleDisbursals, null, null));
return lp;
}
}