From db980236ad24f9a15a92f64e918c629f1eec927a Mon Sep 17 00:00:00 2001 From: Manoj Date: Tue, 18 Aug 2020 23:23:06 +0530 Subject: [PATCH] FINERACT-1109 appropriate-interest-larger-than-emi --- ...nRescheduleOnDecliningBalanceLoanTest.java | 220 ++++++++++++++++++ .../common/GlobalConfigurationHelper.java | 14 +- .../domain/ConfigurationDomainService.java | 2 + .../domain/ConfigurationDomainServiceJpa.java | 5 + .../data/ScheduleGeneratorDTO.java | 7 +- .../portfolio/loanaccount/domain/Loan.java | 6 +- .../domain/AbstractLoanScheduleGenerator.java | 22 +- ...gBalanceInterestLoanScheduleGenerator.java | 28 +++ .../domain/LoanApplicationTerms.java | 34 ++- .../LoanScheduleModelRepaymentPeriod.java | 2 +- .../RescheduleLoansApiConstants.java | 1 - .../loanaccount/service/LoanUtilService.java | 6 +- .../V361__conf_interest_appropriations.sql | 22 ++ 13 files changed, 351 insertions(+), 18 deletions(-) create mode 100644 fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java create mode 100644 fineract-provider/src/main/resources/sql/migrations/core_db/V361__conf_interest_appropriations.sql diff --git a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java new file mode 100644 index 000000000..4a7ae3aad --- /dev/null +++ b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java @@ -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); + + } + +} diff --git a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java index cfd43a5ea..39657e281 100644 --- a/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java +++ b/fineract-provider/src/integrationTest/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java @@ -88,9 +88,9 @@ public class GlobalConfigurationHelper { ArrayList expectedGlobalConfigurations = getAllDefaultGlobalConfigurations(); ArrayList actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec); - // There are currently 29 global configurations. - Assertions.assertEquals(29, expectedGlobalConfigurations.size()); - Assertions.assertEquals(29, actualGlobalConfigurations.size()); + // There are currently 30 global configurations. + Assertions.assertEquals(30, expectedGlobalConfigurations.size()); + Assertions.assertEquals(30, actualGlobalConfigurations.size()); for (int i = 0; i < expectedGlobalConfigurations.size(); i++) { @@ -354,6 +354,14 @@ public class GlobalConfigurationHelper { isFirstPaydayAllowedOnHoliday.put("trapDoor", false); defaults.add(isFirstPaydayAllowedOnHoliday); + HashMap 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; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java index 2632ae6ec..cc1b1994a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java @@ -96,4 +96,6 @@ public interface ConfigurationDomainService { boolean isSubRatesEnabled(); boolean isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); + + boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java index a037f2ce3..321d640d6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java @@ -257,6 +257,11 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService 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 public Long retreivePeroidInNumberOfDaysForSkipMeetingDate() { final String propertyName = "skip-repayment-on-first-day-of-month"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java index 81571acc6..c14403f6b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/ScheduleGeneratorDTO.java @@ -44,6 +44,7 @@ public class ScheduleGeneratorDTO { final boolean isSkipRepaymentOnFirstDayofMonth; final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; final boolean isFirstRepaymentDateAllowedOnHoliday; + final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO, @@ -52,7 +53,7 @@ public class ScheduleGeneratorDTO { final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Boolean isInterestChargedFromDateAsDisbursementDateEnabled, final Integer numberOfdays, final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, - final boolean isFirstRepaymentDateAllowedOnHoliday) { + final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI) { this.loanScheduleFactory = loanScheduleFactory; this.applicationCurrency = applicationCurrency; @@ -70,6 +71,7 @@ public class ScheduleGeneratorDTO { this.isSkipRepaymentOnFirstDayofMonth = isSkipRepaymentOnFirstDayofMonth; this.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; + this.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; } public LoanScheduleGeneratorFactory getLoanScheduleFactory() { @@ -148,4 +150,7 @@ public class ScheduleGeneratorDTO { return isFirstRepaymentDateAllowedOnHoliday; } + public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI() { + return isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index e9e385af1..9a419b19f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -5486,7 +5486,8 @@ public class Loan extends AbstractPersistableCustom { compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, calendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), - holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday()); + holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(), + scheduleGeneratorDTO.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI()); return loanApplicationTerms; } @@ -5764,7 +5765,8 @@ public class Loan extends AbstractPersistableCustom { this.loanProduct.getInstallmentAmountInMultiplesOf(), recalculationFrequencyType, restCalendarInstance, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, - calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false); + calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false, + false); } /** diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index ae697599b..193017059 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -60,7 +60,7 @@ import org.joda.time.LocalDate; public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGenerator { - private final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); + protected final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); @Override @@ -282,7 +282,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // will check for EMI amount greater than interest calculated 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(); throw new MultiDisbursementEmiAmountException(errorMsg, principalInterestForThisPeriod.interest().getAmount(), loanApplicationTerms.getFixedEmiAmount()); @@ -369,6 +370,23 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener 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) periods) { + installment.addInterestAmount(interestFraction); + } + LoanScheduleModelPeriod installment = ((List) 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 // loan interest updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java index 2af9487a3..defae9228 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java @@ -128,6 +128,34 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS interestForThisInstallment = interestForThisInstallment.plus(result.interest()); 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; if (interestForPeriod.isGreaterThanZero()) { interestForPeriod = interestForPeriod.minus(cumulatingInterestPaymentDueToGrace); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 2789796e6..5f5ba829a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -192,6 +192,8 @@ public final class LoanApplicationTerms { private final boolean isFirstRepaymentDateAllowedOnHoliday; + private final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + private final HolidayDetailDTO holidayDetailDTO; private final Set periodNumbersApplicableForPrincipalGrace = new HashSet<>(); @@ -208,6 +210,7 @@ public final class LoanApplicationTerms { private int periodsCompleted = 0; private int extraPeriods = 0; private boolean isEqualAmortization; + private Money interestTobeApproppriated; public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, @@ -245,7 +248,7 @@ public final class LoanApplicationTerms { recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, 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, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy, 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, @@ -289,7 +293,7 @@ public final class LoanApplicationTerms { BigDecimal annualNominalInterestRate, final List loanTermVariations, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Integer numberOfDays, 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 repaymentEvery = loanProductRelatedDetail.getRepayEvery(); @@ -327,7 +331,8 @@ public final class LoanApplicationTerms { compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, - allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday); + allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday, + isInterestToBeAppropriatedEquallyWhenGreaterThanEMI); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, @@ -386,7 +391,7 @@ public final class LoanApplicationTerms { recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, 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.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO, applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization, - applicationTerms.isFirstRepaymentDateAllowedOnHoliday); + applicationTerms.isFirstRepaymentDateAllowedOnHoliday, + applicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -437,7 +443,8 @@ public final class LoanApplicationTerms { BigDecimal approvedAmount, List loanTermVariations, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, 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.loanTermFrequency = loanTermFrequency; @@ -511,6 +518,7 @@ public final class LoanApplicationTerms { this.totalPrincipalAccounted = principal.zero(); this.isEqualAmortization = isEqualAmortization; this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; + this.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -1778,4 +1786,16 @@ public final class LoanApplicationTerms { return isFirstRepaymentDateAllowedOnHoliday; } + public Money getInterestTobeApproppriated() { + return interestTobeApproppriated; + } + + public void setInterestTobeApproppriated(Money interestTobeApproppriated) { + this.interestTobeApproppriated = interestTobeApproppriated; + } + + public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled() { + return isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java index 3cb035f6c..b4720b6c3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelRepaymentPeriod.java @@ -154,7 +154,7 @@ public final class LoanScheduleModelRepaymentPeriod implements LoanScheduleModel @Override public void addInterestAmount(Money interestDue) { this.interestDue = this.interestDue.plus(interestDue); - this.totalDue = this.totalDue.plus(principalDue); + this.totalDue = this.totalDue.plus(interestDue); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java index 7cbdc37c6..b87153663 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/RescheduleLoansApiConstants.java @@ -52,7 +52,6 @@ public class RescheduleLoansApiConstants { public static final String approveCommandParamName = "approve"; public static final String pendingCommandParamName = "pending"; public static final String rejectCommandParamName = "reject"; - public static final String endDateParamName = "endDate"; public static final String emiParamName = "emi"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java index 2c2c89454..1c4302191 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java @@ -143,11 +143,15 @@ public class LoanUtilService { boolean isFirstRepaymentDateAllowedOnHoliday = this.configurationDomainService .isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); + boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = this.configurationDomainService + .isInterestToBeAppropriatedEquallyWhenGreaterThanEMI(); + ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency, calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom, overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper, isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth, - isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday); + isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday, + isInterestToBeAppropriatedEquallyWhenGreaterThanEMI); return scheduleGeneratorDTO; } diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V361__conf_interest_appropriations.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V361__conf_interest_appropriations.sql new file mode 100644 index 000000000..a301631bc --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V361__conf_interest_appropriations.sql @@ -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');