Merge pull request #1220 from fynmanoj/AL-14

FINERACT-1109 appropriate-interest-larger-than-emi
This commit is contained in:
Avik Ganguly
2020-08-19 20:20:26 +05:30
committed by GitHub
13 changed files with 351 additions and 18 deletions
@@ -0,0 +1,220 @@
/**
* 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.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanRescheduleRequestTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoanRescheduleOnDecliningBalanceLoanTest {
private static final Logger LOG = LoggerFactory.getLogger(LoanRescheduleOnDecliningBalanceLoanTest.class);
private ResponseSpecification responseSpec;
private ResponseSpecification generalResponseSpec;
private RequestSpecification requestSpec;
private LoanTransactionHelper loanTransactionHelper;
private LoanRescheduleRequestHelper loanRescheduleRequestHelper;
private Integer clientId;
private Integer loanProductId;
private Integer loanId;
private Integer loanRescheduleRequestId;
private final String loanPrincipalAmount = "100000.00";
private final String numberOfRepayments = "12";
private final String interestRatePerPeriod = "18";
private final String dateString = "4 September 2014";
@BeforeEach
public void initialize() {
Utils.initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
this.loanRescheduleRequestHelper = new LoanRescheduleRequestHelper(this.requestSpec, this.responseSpec);
this.generalResponseSpec = new ResponseSpecBuilder().build();
// create all required entities
this.createRequiredEntities();
}
@AfterEach
public void tearDown() {
disableConfig();
}
/**
* Creates the client, loan product, and loan entities
**/
private void createRequiredEntities() {
this.createClientEntity();
this.createLoanProductEntity();
this.createLoanEntity();
this.enableConfig();
}
/**
* create a new client
**/
private void createClientEntity() {
this.clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec);
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, this.clientId);
}
/**
* create a new loan product
**/
private void createLoanProductEntity() {
LOG.info("---------------------------------CREATING LOAN PRODUCT------------------------------------------");
final String loanProductJSON = new LoanProductTestBuilder().withPrincipal(loanPrincipalAmount)
.withNumberOfRepayments(numberOfRepayments).withinterestRatePerPeriod(interestRatePerPeriod)
.withInterestRateFrequencyTypeAsYear().withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeAsDays()
.build(null);
this.loanProductId = this.loanTransactionHelper.getLoanProductId(loanProductJSON);
LOG.info("Successfully created loan product (ID:{}) ", this.loanProductId);
}
/**
* submit a new loan application, approve and disburse the loan
**/
private void createLoanEntity() {
LOG.info("---------------------------------NEW LOAN APPLICATION------------------------------------------");
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(loanPrincipalAmount)
.withLoanTermFrequency(numberOfRepayments).withLoanTermFrequencyAsMonths().withNumberOfRepayments(numberOfRepayments)
.withRepaymentEveryAfter("1").withRepaymentFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments()
.withInterestCalculationPeriodTypeAsDays().withInterestRatePerPeriod(interestRatePerPeriod)
.withInterestTypeAsDecliningBalance().withSubmittedOnDate(dateString).withExpectedDisbursementDate(dateString)
.withPrincipalGrace("2").withInterestGrace("2").build(this.clientId.toString(), this.loanProductId.toString(), null);
this.loanId = this.loanTransactionHelper.getLoanId(loanApplicationJSON);
LOG.info("Sucessfully created loan (ID: {} )", this.loanId);
this.approveLoanApplication();
this.disburseLoan();
}
/**
* approve the loan application
**/
private void approveLoanApplication() {
if (this.loanId != null) {
this.loanTransactionHelper.approveLoan(this.dateString, this.loanId);
LOG.info("Successfully approved loan (ID: {} )", this.loanId);
}
}
/**
* disburse the newly created loan
**/
private void disburseLoan() {
if (this.loanId != null) {
this.loanTransactionHelper.disburseLoan(this.dateString, this.loanId);
LOG.info("Successfully disbursed loan (ID: {} )", this.loanId);
}
}
/**
* enables the configuration `is-interest-to-be-appropriated-equally-when-greater-than-emi`
**/
private void enableConfig() {
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "34", true);
}
/**
* disables the configuration `is-interest-to-be-appropriated-equally-when-greater-than-emi`
**/
private void disableConfig() {
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "34", false);
}
@Test
public void testCreateLoanRescheduleRequestWithInterestAppropriation() {
this.createAndApproveLoanRescheduleRequestForInterestAppropriation();
}
/**
* create new loan reschedule request
**/
private void createAndApproveLoanRescheduleRequestForInterestAppropriation() {
LOG.info(
"---------------------------------CREATING LOAN RESCHEDULE REQUEST FOR INTEREST APPROPRIATTION-------------------------------------");
final String requestJSON = new LoanRescheduleRequestTestBuilder().updateGraceOnPrincipal(null).updateGraceOnInterest(null)
.updateExtraTerms(null).updateRescheduleFromDate("04 January 2015").updateAdjustedDueDate("04 October 2015")
.updateRecalculateInterest(true).build(this.loanId.toString());
this.loanRescheduleRequestId = this.loanRescheduleRequestHelper.createLoanRescheduleRequest(requestJSON);
this.loanRescheduleRequestHelper.verifyCreationOfLoanRescheduleRequest(this.loanRescheduleRequestId);
LOG.info("Successfully created loan reschedule request (ID: {} )", this.loanRescheduleRequestId);
final String aproveRequestJSON = new LoanRescheduleRequestTestBuilder().getApproveLoanRescheduleRequestJSON();
this.loanRescheduleRequestHelper.approveLoanRescheduleRequest(this.loanRescheduleRequestId, aproveRequestJSON);
final HashMap response = (HashMap) this.loanRescheduleRequestHelper.getLoanRescheduleRequest(loanRescheduleRequestId, "statusEnum");
assertTrue((Boolean) response.get("approved"));
LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId);
final Map repaymentSchedule = (Map) this.loanTransactionHelper.getLoanDetail(requestSpec, generalResponseSpec, loanId,
"repaymentSchedule");
final ArrayList periods = (ArrayList) repaymentSchedule.get("periods");
HashMap period = (HashMap) periods.get(5);
Float totalDueForPeriod = (Float) period.get("totalDueForPeriod");
final HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId);
final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment");
assertEquals(12186, totalDueForPeriod.intValue(), "TOTAL EXPECTED LAST REPAYMENT is NOK");
assertEquals(123682, totalExpectedRepayment.intValue(), "TOTAL EXPECTED LAST REPAYMENT is NOK");
LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId);
}
}
@@ -88,9 +88,9 @@ public class GlobalConfigurationHelper {
ArrayList<HashMap> expectedGlobalConfigurations = getAllDefaultGlobalConfigurations(); ArrayList<HashMap> expectedGlobalConfigurations = getAllDefaultGlobalConfigurations();
ArrayList<HashMap> actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec); ArrayList<HashMap> actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec);
// There are currently 29 global configurations. // There are currently 30 global configurations.
Assertions.assertEquals(29, expectedGlobalConfigurations.size()); Assertions.assertEquals(30, expectedGlobalConfigurations.size());
Assertions.assertEquals(29, actualGlobalConfigurations.size()); Assertions.assertEquals(30, actualGlobalConfigurations.size());
for (int i = 0; i < expectedGlobalConfigurations.size(); i++) { for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
@@ -354,6 +354,14 @@ public class GlobalConfigurationHelper {
isFirstPaydayAllowedOnHoliday.put("trapDoor", false); isFirstPaydayAllowedOnHoliday.put("trapDoor", false);
defaults.add(isFirstPaydayAllowedOnHoliday); defaults.add(isFirstPaydayAllowedOnHoliday);
HashMap<String, Object> isInterestAppropriationEnabled = new HashMap<>();
isInterestAppropriationEnabled.put("id", 34);
isInterestAppropriationEnabled.put("name", "is-interest-to-be-appropriated-equally-when-greater-than-emi");
isInterestAppropriationEnabled.put("value", 0);
isInterestAppropriationEnabled.put("enabled", false);
isInterestAppropriationEnabled.put("trapDoor", false);
defaults.add(isInterestAppropriationEnabled);
return defaults; return defaults;
} }
@@ -96,4 +96,6 @@ public interface ConfigurationDomainService {
boolean isSubRatesEnabled(); boolean isSubRatesEnabled();
boolean isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); boolean isFirstRepaymentDateAfterRescheduleAllowedOnHoliday();
boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI();
} }
@@ -257,6 +257,11 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
return getGlobalConfigurationPropertyData("loan-reschedule-is-first-payday-allowed-on-holiday").isEnabled(); return getGlobalConfigurationPropertyData("loan-reschedule-is-first-payday-allowed-on-holiday").isEnabled();
} }
@Override
public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI() {
return getGlobalConfigurationPropertyData("is-interest-to-be-appropriated-equally-when-greater-than-emi").isEnabled();
}
@Override @Override
public Long retreivePeroidInNumberOfDaysForSkipMeetingDate() { public Long retreivePeroidInNumberOfDaysForSkipMeetingDate() {
final String propertyName = "skip-repayment-on-first-day-of-month"; final String propertyName = "skip-repayment-on-first-day-of-month";
@@ -44,6 +44,7 @@ public class ScheduleGeneratorDTO {
final boolean isSkipRepaymentOnFirstDayofMonth; final boolean isSkipRepaymentOnFirstDayofMonth;
final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled;
final boolean isFirstRepaymentDateAllowedOnHoliday; final boolean isFirstRepaymentDateAllowedOnHoliday;
final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI;
public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency applicationCurrency, public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency applicationCurrency,
final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO, final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO,
@@ -52,7 +53,7 @@ public class ScheduleGeneratorDTO {
final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper,
final Boolean isInterestChargedFromDateAsDisbursementDateEnabled, final Integer numberOfdays, final Boolean isInterestChargedFromDateAsDisbursementDateEnabled, final Integer numberOfdays,
final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled,
final boolean isFirstRepaymentDateAllowedOnHoliday) { final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI) {
this.loanScheduleFactory = loanScheduleFactory; this.loanScheduleFactory = loanScheduleFactory;
this.applicationCurrency = applicationCurrency; this.applicationCurrency = applicationCurrency;
@@ -70,6 +71,7 @@ public class ScheduleGeneratorDTO {
this.isSkipRepaymentOnFirstDayofMonth = isSkipRepaymentOnFirstDayofMonth; this.isSkipRepaymentOnFirstDayofMonth = isSkipRepaymentOnFirstDayofMonth;
this.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; this.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled;
this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday;
this.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = isInterestToBeAppropriatedEquallyWhenGreaterThanEMI;
} }
public LoanScheduleGeneratorFactory getLoanScheduleFactory() { public LoanScheduleGeneratorFactory getLoanScheduleFactory() {
@@ -148,4 +150,7 @@ public class ScheduleGeneratorDTO {
return isFirstRepaymentDateAllowedOnHoliday; return isFirstRepaymentDateAllowedOnHoliday;
} }
public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI() {
return isInterestToBeAppropriatedEquallyWhenGreaterThanEMI;
}
} }
@@ -5486,7 +5486,8 @@ public class Loan extends AbstractPersistableCustom {
compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(),
rescheduleStrategyMethod, calendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, rescheduleStrategyMethod, calendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(),
holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday()); holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(),
scheduleGeneratorDTO.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI());
return loanApplicationTerms; return loanApplicationTerms;
} }
@@ -5764,7 +5765,8 @@ public class Loan extends AbstractPersistableCustom {
this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod, this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod,
compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(),
rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false); calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false,
false);
} }
/** /**
@@ -60,7 +60,7 @@ import org.joda.time.LocalDate;
public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGenerator { public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGenerator {
private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); protected final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator();
@Override @Override
@@ -282,7 +282,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// will check for EMI amount greater than interest calculated // will check for EMI amount greater than interest calculated
if (loanApplicationTerms.getFixedEmiAmount() != null if (loanApplicationTerms.getFixedEmiAmount() != null
&& loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) < 0) { && loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) < 0
&& !loanApplicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled()) {
String errorMsg = "EMI amount must be greater than : " + principalInterestForThisPeriod.interest().getAmount(); String errorMsg = "EMI amount must be greater than : " + principalInterestForThisPeriod.interest().getAmount();
throw new MultiDisbursementEmiAmountException(errorMsg, principalInterestForThisPeriod.interest().getAmount(), throw new MultiDisbursementEmiAmountException(errorMsg, principalInterestForThisPeriod.interest().getAmount(),
loanApplicationTerms.getFixedEmiAmount()); loanApplicationTerms.getFixedEmiAmount());
@@ -369,6 +370,23 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(Money.zero(currency)); scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(Money.zero(currency));
} }
if (loanApplicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled()
&& loanApplicationTerms.getInterestTobeApproppriated() != null
&& loanApplicationTerms.getInterestTobeApproppriated().isGreaterThanZero()) {
Money interestFraction = loanApplicationTerms.getInterestTobeApproppriated().dividedBy(periods.size(), mc.getRoundingMode());
BigDecimal roundFraction = interestFraction.getAmount().remainder(BigDecimal.ONE);
interestFraction = interestFraction.minus(roundFraction);
roundFraction = roundFraction.multiply(new BigDecimal(periods.size()));
for (LoanScheduleModelPeriod installment : (List<LoanScheduleModelPeriod>) periods) {
installment.addInterestAmount(interestFraction);
}
LoanScheduleModelPeriod installment = ((List<LoanScheduleModelPeriod>) periods).get(periods.size() - 1);
installment.addInterestAmount(Money.of(currency, roundFraction));
scheduleParams.addTotalRepaymentExpected(loanApplicationTerms.getInterestTobeApproppriated());
scheduleParams.addTotalCumulativeInterest(loanApplicationTerms.getInterestTobeApproppriated());
loanApplicationTerms.setInterestTobeApproppriated(Money.zero(currency));
}
// determine fees and penalties for charges which depends on total // determine fees and penalties for charges which depends on total
// loan interest // loan interest
updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges); updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges);
@@ -128,6 +128,34 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS
interestForThisInstallment = interestForThisInstallment.plus(result.interest()); interestForThisInstallment = interestForThisInstallment.plus(result.interest());
cumulatingInterestDueToGrace = result.interestPaymentDueToGrace(); cumulatingInterestDueToGrace = result.interestPaymentDueToGrace();
Money interestTobeApproppriated = loanApplicationTerms.getInterestTobeApproppriated() == null
? Money.zero(interestForThisInstallment.getCurrency())
: loanApplicationTerms.getInterestTobeApproppriated();
if (loanApplicationTerms.getFixedEmiAmount() != null
&& loanApplicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled() && interestForThisInstallment
.isGreaterThan(Money.of(interestForThisInstallment.getCurrency(), loanApplicationTerms.getFixedEmiAmount()))) {
LocalDate actualPeriodEndDate = this.scheduledDateGenerator.generateNextRepaymentDate(interestStartDate, loanApplicationTerms,
false);
PrincipalInterest tempInterest = loanApplicationTerms.calculateTotalInterestForPeriod(calculator,
interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace,
balanceForInterestCalculation, interestStartDate, actualPeriodEndDate);
Money fixedEmi = Money.of(interestForThisInstallment.getCurrency(), loanApplicationTerms.getFixedEmiAmount());
if (tempInterest.interest().isGreaterThan(fixedEmi)) {
loanApplicationTerms
.setInterestTobeApproppriated(interestTobeApproppriated.plus(interestForThisInstallment.minus(fixedEmi)));
interestForThisInstallment = fixedEmi;
} else {
loanApplicationTerms.setInterestTobeApproppriated(
interestTobeApproppriated.plus(interestForThisInstallment.minus(tempInterest.interest())));
interestForThisInstallment = tempInterest.interest();
}
}
cumulatingInterestDueToGrace = result.interestPaymentDueToGrace();
Money interestForPeriod = interestForThisInstallment; Money interestForPeriod = interestForThisInstallment;
if (interestForPeriod.isGreaterThanZero()) { if (interestForPeriod.isGreaterThanZero()) {
interestForPeriod = interestForPeriod.minus(cumulatingInterestPaymentDueToGrace); interestForPeriod = interestForPeriod.minus(cumulatingInterestPaymentDueToGrace);
@@ -192,6 +192,8 @@ public final class LoanApplicationTerms {
private final boolean isFirstRepaymentDateAllowedOnHoliday; private final boolean isFirstRepaymentDateAllowedOnHoliday;
private final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI;
private final HolidayDetailDTO holidayDetailDTO; private final HolidayDetailDTO holidayDetailDTO;
private final Set<Integer> periodNumbersApplicableForPrincipalGrace = new HashSet<>(); private final Set<Integer> periodNumbersApplicableForPrincipalGrace = new HashSet<>();
@@ -208,6 +210,7 @@ public final class LoanApplicationTerms {
private int periodsCompleted = 0; private int periodsCompleted = 0;
private int extraPeriods = 0; private int extraPeriods = 0;
private boolean isEqualAmortization; private boolean isEqualAmortization;
private Money interestTobeApproppriated;
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
@@ -245,7 +248,7 @@ public final class LoanApplicationTerms {
recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment,
installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations,
calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays,
isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false); isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false);
} }
@@ -271,7 +274,8 @@ public final class LoanApplicationTerms {
principalThresholdForLastInstalment, installmentAmountInMultiplesOf, recalculationFrequencyType, restCalendarInstance, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, recalculationFrequencyType, restCalendarInstance,
compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy,
rescheduleStrategyMethod, loanCalendar, approvedAmount, annualNominalInterestRate, loanTermVariations, rescheduleStrategyMethod, loanCalendar, approvedAmount, annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, false); calendarHistoryDataWrapper, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, false,
false);
} }
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
@@ -289,7 +293,7 @@ public final class LoanApplicationTerms {
BigDecimal annualNominalInterestRate, final List<LoanTermVariationsData> loanTermVariations, BigDecimal annualNominalInterestRate, final List<LoanTermVariationsData> loanTermVariations,
final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Integer numberOfDays, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Integer numberOfDays,
final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod, final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod,
final boolean isFirstRepaymentDateAllowedOnHoliday) { final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI) {
final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments();
final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery();
@@ -327,7 +331,8 @@ public final class LoanApplicationTerms {
compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf,
loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper,
isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO,
allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday); allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday,
isInterestToBeAppropriatedEquallyWhenGreaterThanEMI);
} }
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
@@ -386,7 +391,7 @@ public final class LoanApplicationTerms {
recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment,
installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations,
calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays,
isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false); isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false);
} }
@@ -413,7 +418,8 @@ public final class LoanApplicationTerms {
applicationTerms.calendarHistoryDataWrapper, applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled, applicationTerms.calendarHistoryDataWrapper, applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled,
applicationTerms.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO, applicationTerms.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO,
applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization, applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization,
applicationTerms.isFirstRepaymentDateAllowedOnHoliday); applicationTerms.isFirstRepaymentDateAllowedOnHoliday,
applicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI);
} }
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
@@ -437,7 +443,8 @@ public final class LoanApplicationTerms {
BigDecimal approvedAmount, List<LoanTermVariationsData> loanTermVariations, BigDecimal approvedAmount, List<LoanTermVariationsData> loanTermVariations,
final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled,
final Integer numberOfDays, final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final Integer numberOfDays, final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO,
final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isFirstRepaymentDateAllowedOnHoliday) { final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isFirstRepaymentDateAllowedOnHoliday,
final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI) {
this.currency = currency; this.currency = currency;
this.loanTermFrequency = loanTermFrequency; this.loanTermFrequency = loanTermFrequency;
@@ -511,6 +518,7 @@ public final class LoanApplicationTerms {
this.totalPrincipalAccounted = principal.zero(); this.totalPrincipalAccounted = principal.zero();
this.isEqualAmortization = isEqualAmortization; this.isEqualAmortization = isEqualAmortization;
this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday;
this.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = isInterestToBeAppropriatedEquallyWhenGreaterThanEMI;
} }
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -1778,4 +1786,16 @@ public final class LoanApplicationTerms {
return isFirstRepaymentDateAllowedOnHoliday; return isFirstRepaymentDateAllowedOnHoliday;
} }
public Money getInterestTobeApproppriated() {
return interestTobeApproppriated;
}
public void setInterestTobeApproppriated(Money interestTobeApproppriated) {
this.interestTobeApproppriated = interestTobeApproppriated;
}
public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled() {
return isInterestToBeAppropriatedEquallyWhenGreaterThanEMI;
}
} }
@@ -154,7 +154,7 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel
@Override @Override
public void addInterestAmount(Money interestDue) { public void addInterestAmount(Money interestDue) {
this.interestDue = this.interestDue.plus(interestDue); this.interestDue = this.interestDue.plus(interestDue);
this.totalDue = this.totalDue.plus(principalDue); this.totalDue = this.totalDue.plus(interestDue);
} }
@Override @Override
@@ -52,7 +52,6 @@ public class RescheduleLoansApiConstants {
public static final String approveCommandParamName = "approve"; public static final String approveCommandParamName = "approve";
public static final String pendingCommandParamName = "pending"; public static final String pendingCommandParamName = "pending";
public static final String rejectCommandParamName = "reject"; public static final String rejectCommandParamName = "reject";
public static final String endDateParamName = "endDate"; public static final String endDateParamName = "endDate";
public static final String emiParamName = "emi"; public static final String emiParamName = "emi";
@@ -143,11 +143,15 @@ public class LoanUtilService {
boolean isFirstRepaymentDateAllowedOnHoliday = this.configurationDomainService boolean isFirstRepaymentDateAllowedOnHoliday = this.configurationDomainService
.isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); .isFirstRepaymentDateAfterRescheduleAllowedOnHoliday();
boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = this.configurationDomainService
.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI();
ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency, ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom, calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom,
overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper, overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper,
isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth, isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth,
isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday); isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday,
isInterestToBeAppropriatedEquallyWhenGreaterThanEMI);
return scheduleGeneratorDTO; return scheduleGeneratorDTO;
} }
@@ -0,0 +1,22 @@
--
-- 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.
--
INSERT INTO `c_configuration` (`name`, `value`, `date_value`, `enabled`, `is_trap_door`, `description`)
VALUES
('is-interest-to-be-appropriated-equally-when-greater-than-emi', 0, NULL, 0, 0, 'If enabled, while loan reschedule when interest amount is greater than EMI, the additional interest is spread equally over remaining installments');