From 7c235c583b5fe2b85f2b4cbd8a53f59c05d0fc46 Mon Sep 17 00:00:00 2001 From: Manoj <56669674+fynmanoj@users.noreply.github.com> Date: Fri, 18 Feb 2022 23:52:51 +0530 Subject: [PATCH] FINERACT-1443-loan-interest-recover-policy-fix-when-interest-more-than-EMI (#2033) --- .../domain/ConfigurationDomainService.java | 6 +- .../domain/ConfigurationDomainServiceJpa.java | 9 +- .../data/ScheduleGeneratorDTO.java | 17 ++- .../portfolio/loanaccount/domain/Loan.java | 5 +- .../domain/AbstractLoanScheduleGenerator.java | 70 ++-------- ...gBalanceInterestLoanScheduleGenerator.java | 48 +++---- .../domain/DefaultScheduledDateGenerator.java | 18 +++ .../domain/LoanApplicationTerms.java | 63 ++++++--- .../domain/PrincipalInterest.java | 9 ++ .../domain/ScheduledDateGenerator.java | 3 + .../service/LoanScheduleAssembler.java | 8 +- .../loanaccount/service/LoanUtilService.java | 8 +- ...__interest_recovery_conf_for_rescedule.sql | 29 ++++ ...nRescheduleOnDecliningBalanceLoanTest.java | 130 +++++++++++++++--- .../common/GlobalConfigurationHelper.java | 28 ++-- .../loans/LoanApplicationTestBuilder.java | 7 + 16 files changed, 310 insertions(+), 148 deletions(-) create mode 100644 fineract-provider/src/main/resources/sql/migrations/core_db/V392__interest_recovery_conf_for_rescedule.sql 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 8a2a7268e..037f4d6a2 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 @@ -75,6 +75,10 @@ public interface ConfigurationDomainService { boolean isSkippingMeetingOnFirstDayOfMonthEnabled(); + boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI(); + + boolean isPrincipalCompoundingDisabledForOverdueLoans(); + Long retreivePeroidInNumberOfDaysForSkipMeetingDate(); boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled(); @@ -97,8 +101,6 @@ public interface ConfigurationDomainService { boolean isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); - boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI(); - String getAccountMappingForPaymentType(); String getAccountMappingForCharge(); 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 6b6c5957b..30426ef74 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 @@ -259,8 +259,13 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService } @Override - public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI() { - return getGlobalConfigurationPropertyData("is-interest-to-be-appropriated-equally-when-greater-than-emi").isEnabled(); + public boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI() { + return getGlobalConfigurationPropertyData("is-interest-to-be-recovered-first-when-greater-than-emi").isEnabled(); + } + + @Override + public boolean isPrincipalCompoundingDisabledForOverdueLoans() { + return getGlobalConfigurationPropertyData("is-principal-compounding-disabled-for-overdue-loans").isEnabled(); } @Override 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 4d609bfcf..8fc33719e 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,7 +44,8 @@ public class ScheduleGeneratorDTO { final boolean isSkipRepaymentOnFirstDayofMonth; final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; final boolean isFirstRepaymentDateAllowedOnHoliday; - final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI; + final boolean isPrincipalCompoundingDisabledForOverdueLoans; public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency applicationCurrency, final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO, @@ -53,7 +54,8 @@ 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 isInterestToBeAppropriatedEquallyWhenGreaterThanEMI) { + final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, + final boolean isPrincipalCompoundingDisabledForOverdueLoans) { this.loanScheduleFactory = loanScheduleFactory; this.applicationCurrency = applicationCurrency; @@ -71,7 +73,8 @@ public class ScheduleGeneratorDTO { this.isSkipRepaymentOnFirstDayofMonth = isSkipRepaymentOnFirstDayofMonth; this.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; - this.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + this.isInterestToBeRecoveredFirstWhenGreaterThanEMI = isInterestToBeRecoveredFirstWhenGreaterThanEMI; + this.isPrincipalCompoundingDisabledForOverdueLoans = isPrincipalCompoundingDisabledForOverdueLoans; } public LoanScheduleGeneratorFactory getLoanScheduleFactory() { @@ -150,7 +153,11 @@ public class ScheduleGeneratorDTO { return isFirstRepaymentDateAllowedOnHoliday; } - public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI() { - return isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + public boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI() { + return isInterestToBeRecoveredFirstWhenGreaterThanEMI; + } + + public boolean isPrincipalCompoundingDisabledForOverdueLoans() { + return isPrincipalCompoundingDisabledForOverdueLoans; } } 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 104ddea5e..0cfe9b566 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 @@ -5623,7 +5623,8 @@ public class Loan extends AbstractPersistableCustom { rescheduleStrategyMethod, calendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(), - scheduleGeneratorDTO.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI(), this.fixedPrincipalPercentagePerInstallment); + scheduleGeneratorDTO.isInterestToBeRecoveredFirstWhenGreaterThanEMI(), this.fixedPrincipalPercentagePerInstallment, + scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans()); return loanApplicationTerms; } @@ -5905,7 +5906,7 @@ public class Loan extends AbstractPersistableCustom { compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false, - false, this.fixedPrincipalPercentagePerInstallment); + false, this.fixedPrincipalPercentagePerInstallment, 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 94a6f59cf..fd008cfb5 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 @@ -286,8 +286,7 @@ 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.isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled()) { + && loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) < 0) { String errorMsg = "EMI amount must be greater than : " + principalInterestForThisPeriod.interest().getAmount(); throw new MultiDisbursementEmiAmountException(errorMsg, principalInterestForThisPeriod.interest().getAmount(), loanApplicationTerms.getFixedEmiAmount()); @@ -333,7 +332,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(), currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(), totalInstallmentDue, !isCompletePeriod); - + if (principalInterestForThisPeriod.getRescheduleInterestPortion() != null) { + installment.setRescheduleInterestPortion(principalInterestForThisPeriod.getRescheduleInterestPortion().getAmount()); + } addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment); // apply loan transactions on installments to identify early/late // payments for interest recalculation @@ -379,44 +380,6 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(Money.zero(currency)); } - if (loanApplicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled() - && loanApplicationTerms.getInterestTobeApproppriated() != null - && loanApplicationTerms.getInterestTobeApproppriated().isGreaterThanZero()) { - int emisTobeChanged = 1; - for (LoanScheduleModelPeriod installment : (List) periods) { - if (!installment.isEMIFixedSpecificToInstallment()) { - emisTobeChanged++; - } - } - if (emisTobeChanged > 1) { - emisTobeChanged--; - } - Money interestTobeApproppriated = loanApplicationTerms.getInterestTobeApproppriated(); - Money interestFraction = interestTobeApproppriated.dividedBy(emisTobeChanged, mc.getRoundingMode()); - BigDecimal roundFraction = interestFraction.getAmount().remainder(BigDecimal.ONE); - interestFraction = interestFraction.minus(roundFraction); - for (LoanScheduleModelPeriod installment : (List) periods) { - if (!installment.isEMIFixedSpecificToInstallment()) { - installment.addInterestAmount(interestFraction); - installment.setRescheduleInterestPortion(interestFraction.getAmount()); - interestTobeApproppriated = interestTobeApproppriated.minus(interestFraction); - } - } - LoanScheduleModelPeriod lastInstallment = ((List) periods).get(periods.size() - 1); - - if (interestTobeApproppriated.isGreaterThanZero()) { - lastInstallment.addInterestAmount(interestTobeApproppriated); - BigDecimal rescheduleInterestPortion = BigDecimal.ZERO; - if (lastInstallment.rescheduleInterestPortion() != null) { - rescheduleInterestPortion = lastInstallment.rescheduleInterestPortion(); - } - lastInstallment.setRescheduleInterestPortion(rescheduleInterestPortion.add(interestTobeApproppriated.getAmount())); - } - 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); @@ -1054,6 +1017,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener modifiedScheduledDueDate = loanTermVariationsData.getDateValue(); if (!loanTermVariationsData.isSpecificToInstallment()) { scheduleParams.setActualRepaymentDate(modifiedScheduledDueDate); + loanApplicationTerms.setNewScheduledDueDateStart(modifiedScheduledDueDate); } loanTermVariationsData.setProcessed(true); } @@ -2397,23 +2361,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener periods.clear(); } - BigDecimal rescheuleInterestPortionTobeRetained = BigDecimal.ZERO; - for (LoanRepaymentScheduleInstallment retainedInstallment : retainedInstallments) { - if (retainedInstallment.getRescheduleInterestPortion() != null) { - rescheuleInterestPortionTobeRetained = rescheuleInterestPortionTobeRetained - .add(retainedInstallment.getRescheduleInterestPortion()); - } + if (retainedInstallments.size() > 0 + && retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion() != null) { + loanApplicationTerms.setInterestTobeApproppriated( + Money.of(loan.getCurrency(), retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion())); } - - BigDecimal rescheuleInterestPortionTotal = BigDecimal.ZERO; - for (LoanRepaymentScheduleInstallment inst : loan.getRepaymentScheduleInstallments()) { - if (inst.getRescheduleInterestPortion() != null) { - rescheuleInterestPortionTotal = rescheuleInterestPortionTotal.add(inst.getRescheduleInterestPortion()); - } - } - - BigDecimal rescheuleInterestPortionTobeAppropriated = rescheuleInterestPortionTotal.subtract(rescheuleInterestPortionTobeRetained); - loanApplicationTerms.setInterestTobeApproppriated(Money.of(loan.getCurrency(), rescheuleInterestPortionTobeAppropriated)); LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loan.charges(), holidayDetailDTO, loanScheduleParams); for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) { @@ -2640,8 +2592,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final HolidayDetailDTO holidayDetailDTO) { LocalDate nextScheduleDate = null; if (loanApplicationTerms.getRecalculationFrequencyType().isSameAsRepayment()) { - nextScheduleDate = this.scheduledDateGenerator.generateNextScheduleDateStartingFromDisburseDate(startDate, loanApplicationTerms, - holidayDetailDTO); + nextScheduleDate = this.scheduledDateGenerator.generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(startDate, + loanApplicationTerms, holidayDetailDTO); } else { CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance(); nextScheduleDate = CalendarUtils.getNextScheduleDate(calendarInstance.getCalendar(), startDate); 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 a120fa88b..c39112577 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 @@ -113,7 +113,11 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS compoundFee = compoundingMap.get(principal.getKey()); compoundingMap.put(principal.getKey(), interestToBeCompounded.plus(compoundFee)); } - balanceForInterestCalculation = balanceForInterestCalculation.plus(principal.getValue()).plus(compoundFee); + if (!loanApplicationTerms.isPrincipalCompoundingDisabledForOverdueLoans()) { + balanceForInterestCalculation = balanceForInterestCalculation.plus(principal.getValue()); + } + balanceForInterestCalculation = balanceForInterestCalculation.plus(compoundFee); + if (interestRates.containsKey(principal.getKey())) { loanApplicationTerms.updateAnnualNominalInterestRate(interestRates.get(principal.getKey())); } @@ -128,42 +132,27 @@ 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(); - } + if (loanApplicationTerms.isInterestToBeRecoveredFirstWhenGreaterThanEMIEnabled() + && loanApplicationTerms.isInterestTobeApproppriated()) { + interestForThisInstallment = interestForThisInstallment.add(loanApplicationTerms.getInterestTobeApproppriated()); + loanApplicationTerms.setInterestTobeApproppriated(interestForThisInstallment.zero()); } - cumulatingInterestDueToGrace = result.interestPaymentDueToGrace(); - Money interestForPeriod = interestForThisInstallment; if (interestForPeriod.isGreaterThanZero()) { interestForPeriod = interestForPeriod.minus(cumulatingInterestPaymentDueToGrace); } else { interestForPeriod = cumulatingInterestDueToGrace.minus(cumulatingInterestPaymentDueToGrace); } + Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance, periodNumber, mc, interestForPeriod); + if (loanApplicationTerms.isInterestToBeRecoveredFirstWhenGreaterThanEMIEnabled() && principalForThisInstallment.isLessThanZero() + && !loanApplicationTerms.isLastRepaymentPeriod(periodNumber)) { + loanApplicationTerms.setInterestTobeApproppriated(principalForThisInstallment.abs()); + interestForThisInstallment = interestForThisInstallment.minus(loanApplicationTerms.getInterestTobeApproppriated()); + principalForThisInstallment = principalForThisInstallment.zero(); + } // update cumulative fields for principal & interest final Money interestBroughtFowardDueToGrace = cumulatingInterestDueToGrace; @@ -173,6 +162,9 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment, totalCumulativePrincipalToDate, periodNumber); - return new PrincipalInterest(principalForThisInstallment, interestForThisInstallment, interestBroughtFowardDueToGrace); + PrincipalInterest principalInterest = new PrincipalInterest(principalForThisInstallment, interestForThisInstallment, + interestBroughtFowardDueToGrace); + principalInterest.setRescheduleInterestPortion(loanApplicationTerms.getInterestTobeApproppriated()); + return principalInterest; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index d2853d410..584de8cf4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -327,4 +327,22 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { generatedDate = adjustRepaymentDate(generatedDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate(); return generatedDate; } + + @Override + public LocalDate generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(LocalDate lastRepaymentDate, + LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) { + + LocalDate generatedDate = loanApplicationTerms.getExpectedDisbursementDate(); + boolean isFirstRepayment = true; + if (loanApplicationTerms.getNewScheduledDueDateStart() != null) { + generatedDate = loanApplicationTerms.getNewScheduledDueDateStart(); + isFirstRepayment = false; + } + while (!generatedDate.isAfter(lastRepaymentDate)) { + generatedDate = generateNextRepaymentDate(generatedDate, loanApplicationTerms, isFirstRepayment); + isFirstRepayment = false; + } + generatedDate = adjustRepaymentDate(generatedDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate(); + return generatedDate; + } } 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 6386d37bb..11a344bc8 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 @@ -188,7 +188,9 @@ public final class LoanApplicationTerms { private final boolean isFirstRepaymentDateAllowedOnHoliday; - private final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + private final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI; + + private boolean isPrincipalCompoundingDisabledForOverdueLoans; private final HolidayDetailDTO holidayDetailDTO; @@ -209,6 +211,8 @@ public final class LoanApplicationTerms { private Money interestTobeApproppriated; private final BigDecimal fixedPrincipalPercentagePerInstallment; + private LocalDate newScheduledDueDateStart; + public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType, @@ -230,7 +234,8 @@ public final class LoanApplicationTerms { BigDecimal approvedAmount, List loanTermVariations, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final Integer numberOfDays, boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod, - final boolean isEqualAmortization, final BigDecimal fixedPrincipalPercentagePerInstallment) { + final boolean isEqualAmortization, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, + final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; @@ -245,8 +250,9 @@ public final class LoanApplicationTerms { recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, - isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false, - fixedPrincipalPercentagePerInstallment); + isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, + isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, + isPrincipalCompoundingDisabledForOverdueLoans); } @@ -273,7 +279,7 @@ public final class LoanApplicationTerms { compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy, rescheduleStrategyMethod, loanCalendar, approvedAmount, annualNominalInterestRate, loanTermVariations, calendarHistoryDataWrapper, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, false, - false, null); + false, null, false); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, @@ -291,8 +297,8 @@ 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 isInterestToBeAppropriatedEquallyWhenGreaterThanEMI, - final BigDecimal fixedPrincipalPercentagePerInstallment) { + final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, + final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans) { final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); @@ -331,7 +337,8 @@ public final class LoanApplicationTerms { loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday, - isInterestToBeAppropriatedEquallyWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment); + isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, + isPrincipalCompoundingDisabledForOverdueLoans); } public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, @@ -390,7 +397,7 @@ public final class LoanApplicationTerms { recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, - isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false, null); + isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false, null, false); } @@ -417,8 +424,8 @@ public final class LoanApplicationTerms { applicationTerms.calendarHistoryDataWrapper, applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled, applicationTerms.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO, applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization, - applicationTerms.isFirstRepaymentDateAllowedOnHoliday, applicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI, - applicationTerms.fixedPrincipalPercentagePerInstallment); + applicationTerms.isFirstRepaymentDateAllowedOnHoliday, applicationTerms.isInterestToBeRecoveredFirstWhenGreaterThanEMI, + applicationTerms.fixedPrincipalPercentagePerInstallment, applicationTerms.isPrincipalCompoundingDisabledForOverdueLoans); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -443,7 +450,8 @@ public final class LoanApplicationTerms { 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 isInterestToBeAppropriatedEquallyWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment) { + final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment, + final boolean isPrincipalCompoundingDisabledForOverdueLoans) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -517,8 +525,9 @@ public final class LoanApplicationTerms { this.totalPrincipalAccounted = principal.zero(); this.isEqualAmortization = isEqualAmortization; this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; - this.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + this.isInterestToBeRecoveredFirstWhenGreaterThanEMI = isInterestToBeRecoveredFirstWhenGreaterThanEMI; this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment; + this.isPrincipalCompoundingDisabledForOverdueLoans = isPrincipalCompoundingDisabledForOverdueLoans; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -743,6 +752,10 @@ public final class LoanApplicationTerms { return periodNumber == numberOfRepayments; } + public boolean isLastRepaymentPeriod(final int periodNumber) { + return periodNumber == this.actualNumberOfRepayments; + } + /** * general method to calculate totalInterestDue discounting any grace settings */ @@ -1248,7 +1261,7 @@ public final class LoanApplicationTerms { Money interestDue = Money.zero(outstandingBalance.getCurrency()); final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc, this.daysInMonthType, this.daysInYearType, - periodStartDate, periodEndDate); + periodStartDate, periodEndDate);// 0.021232877 ob:14911.64 interestDue = outstandingBalance.multiplyRetainScale(periodicInterestRate, mc.getRoundingMode()); return interestDue; @@ -1334,7 +1347,6 @@ public final class LoanApplicationTerms { private Money calculatePrincipalDueForInstallment(final int periodNumber, final Money totalDuePerInstallment, final Money periodInterest) { - Money principal = totalDuePerInstallment.minus(periodInterest); if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { principal = principal.zero(); @@ -1795,15 +1807,30 @@ public final class LoanApplicationTerms { } public Money getInterestTobeApproppriated() { - return interestTobeApproppriated; + return interestTobeApproppriated == null ? this.principal.zero() : interestTobeApproppriated; } public void setInterestTobeApproppriated(Money interestTobeApproppriated) { this.interestTobeApproppriated = interestTobeApproppriated; } - public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled() { - return isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; + public Boolean isInterestTobeApproppriated() { + return interestTobeApproppriated != null && interestTobeApproppriated.isGreaterThanZero(); } + public boolean isInterestToBeRecoveredFirstWhenGreaterThanEMIEnabled() { + return isInterestToBeRecoveredFirstWhenGreaterThanEMI; + } + + public boolean isPrincipalCompoundingDisabledForOverdueLoans() { + return isPrincipalCompoundingDisabledForOverdueLoans; + } + + public LocalDate getNewScheduledDueDateStart() { + return newScheduledDueDateStart; + } + + public void setNewScheduledDueDateStart(LocalDate newScheduledDueDateStart) { + this.newScheduledDueDateStart = newScheduledDueDateStart; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java index 63c77aae0..3a0e724fa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrincipalInterest.java @@ -25,6 +25,7 @@ public class PrincipalInterest { private final Money principal; private final Money interest; private final Money interestPaymentDueToGrace; + private Money rescheduleInterestPortion; public PrincipalInterest(final Money principal, final Money interest, final Money interestPaymentDueToGrace) { this.principal = principal; @@ -43,4 +44,12 @@ public class PrincipalInterest { public Money interestPaymentDueToGrace() { return this.interestPaymentDueToGrace; } + + public Money getRescheduleInterestPortion() { + return rescheduleInterestPortion; + } + + public void setRescheduleInterestPortion(Money rescheduleInterestPortion) { + this.rescheduleInterestPortion = rescheduleInterestPortion; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java index 969db0ed9..f2e6de21c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java @@ -43,4 +43,7 @@ public interface ScheduledDateGenerator { LocalDate generateNextScheduleDateStartingFromDisburseDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO); + + LocalDate generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(LocalDate lastRepaymentDate, + LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index d41e0e35b..205fb8471 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -448,7 +448,10 @@ public class LoanScheduleAssembler { Date.from(expectedDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), HolidayStatusType.ACTIVE.getValue()); final WorkingDays workingDays = this.workingDaysRepository.findOne(); HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays); - + final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI = this.configurationDomainService + .isInterestToBeRecoveredFirstWhenGreaterThanEMI(); + final boolean isPrincipalCompoundingDisabledForOverdueLoans = this.configurationDomainService + .isPrincipalCompoundingDisabledForOverdueLoans(); return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, @@ -460,7 +463,8 @@ public class LoanScheduleAssembler { compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, installmentAmountInMultiplesOf, loanProduct.preCloseInterestCalculationStrategy(), calendar, BigDecimal.ZERO, loanTermVariations, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipMeetingOnFirstDay, detailDTO, - allowCompoundingOnEod, isEqualAmortization, fixedPrincipalPercentagePerInstallment); + allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI, + fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans); } private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, 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 ee201c8ea..0653a7218 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 @@ -144,15 +144,17 @@ public class LoanUtilService { boolean isFirstRepaymentDateAllowedOnHoliday = this.configurationDomainService .isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); - boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = this.configurationDomainService - .isInterestToBeAppropriatedEquallyWhenGreaterThanEMI(); + boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI = this.configurationDomainService + .isInterestToBeRecoveredFirstWhenGreaterThanEMI(); + boolean isPrincipalCompoundingDisabledForOverdueLoans = this.configurationDomainService + .isPrincipalCompoundingDisabledForOverdueLoans(); ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency, calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom, overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper, isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth, isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday, - isInterestToBeAppropriatedEquallyWhenGreaterThanEMI); + isInterestToBeRecoveredFirstWhenGreaterThanEMI, isPrincipalCompoundingDisabledForOverdueLoans); return scheduleGeneratorDTO; } diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V392__interest_recovery_conf_for_rescedule.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V392__interest_recovery_conf_for_rescedule.sql new file mode 100644 index 000000000..c866dd072 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V392__interest_recovery_conf_for_rescedule.sql @@ -0,0 +1,29 @@ +-- +-- 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. +-- + +delete from c_configuration where name = 'is-interest-to-be-appropriated-equally-when-greater-than-emi'; + +INSERT INTO `c_configuration` (`name`, `value`, `date_value`, `enabled`, `is_trap_door`, `description`) +VALUES + ('is-interest-to-be-recovered-first-when-greater-than-emi', 0, NULL, 0, 0, 'If enabled, when interest amount is greater than EMI, the additional interest is recovered first before princupal'); + + +INSERT INTO `c_configuration` (`name`, `value`, `date_value`, `enabled`, `is_trap_door`, `description`) +VALUES + ('is-principal-compounding-disabled-for-overdue-loans', 0, NULL, 0, 0, 'If enabled, it donot consider principal of an upaid installment for calculating interest of next installment. this is for testing back-dated loan sshedule'); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java index 4734355af..6fb16074f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleOnDecliningBalanceLoanTest.java @@ -225,31 +225,31 @@ public class LoanRescheduleOnDecliningBalanceLoanTest { } /** - * enables the configuration `is-interest-to-be-appropriated-equally-when-greater-than-emi` + * enables the configuration `is-interest-to-be-recovered-first-when-greater-than-emi` **/ private void enableConfig() { - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "34", true); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "42", true); } /** - * disables the configuration `is-interest-to-be-appropriated-equally-when-greater-than-emi` + * disables the configuration `is-interest-to-be-recovered-first-when-greater-than-emi` **/ private void disableConfig() { - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "34", false); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "42", false); } @Test public void testCreateLoanRescheduleRequestWithInterestAppropriation() { // create all required entities this.createRequiredEntities(); - this.createAndApproveLoanRescheduleRequestForInterestAppropriation(); + this.createAndApproveLoanRescheduleRequestForRecoverInterestInterestFirst(); } /** * create new loan reschedule request **/ - private void createAndApproveLoanRescheduleRequestForInterestAppropriation() { + private void createAndApproveLoanRescheduleRequestForRecoverInterestInterestFirst() { LOG.info( "---------------------------------CREATING LOAN RESCHEDULE REQUEST FOR INTEREST APPROPRIATTION-------------------------------------"); @@ -279,8 +279,8 @@ public class LoanRescheduleOnDecliningBalanceLoanTest { final HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId); final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment"); - assertEquals(12186, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); - assertEquals(123682, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK"); + assertEquals(10831, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); + assertEquals(125184, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK"); LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); @@ -327,7 +327,7 @@ public class LoanRescheduleOnDecliningBalanceLoanTest { final HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId); final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment"); - assertEquals(12326, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); + assertEquals(10831, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); assertEquals(131512, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK"); LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); @@ -338,13 +338,13 @@ public class LoanRescheduleOnDecliningBalanceLoanTest { public void testCreateLoanRescheduleRequestForInterestAppropriationAndFixedEMI() { // create all required entities this.createRequiredEntities(); - this.createAndApproveLoanRescheduleRequestForInterestAppropriationAndFixedEMI(); + this.createAndApproveLoanRescheduleRequestForRecoverInterestFirstAndFixedEMI(); } /** - * create new loan reschedule request with combination of date change, interest appropriation and fixed emi + * create new loan reschedule request with combination of date change, recover interest first and fixed emi **/ - private void createAndApproveLoanRescheduleRequestForInterestAppropriationAndFixedEMI() { + private void createAndApproveLoanRescheduleRequestForRecoverInterestFirstAndFixedEMI() { LOG.info( "---------------------------------CREATING LOAN RESCHEDULE REQUEST FOR INTEREST APPROPRIATTION-------------------------------------"); @@ -379,8 +379,8 @@ public class LoanRescheduleOnDecliningBalanceLoanTest { assertEquals(5000, totalFixedDueForPeriod.intValue(), "EXPECTED FIXED REPAYMENT is NOK"); - assertEquals(15316, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); - assertEquals(120806, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK"); + assertEquals(15417, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); + assertEquals(121412, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK"); LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); @@ -390,7 +390,7 @@ public class LoanRescheduleOnDecliningBalanceLoanTest { public void testCreateLoanRescheduleRequestWithMultpleInterestAppropriation() { // create all required entities this.createRequiredEntities(); - this.createAndApproveLoanRescheduleRequestForInterestAppropriation(); + this.createAndApproveLoanRescheduleRequestForRecoverInterestInterestFirst(); this.createAndApproveLoanRescheduleRequestForSecondInterestAppropriation(); @@ -429,9 +429,105 @@ public class LoanRescheduleOnDecliningBalanceLoanTest { final HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId); final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment"); - assertEquals(12187, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT in Second Reschedule is NOK"); - assertEquals(130750, totalExpectedRepayment.intValue(), "TOTAL EXPECTED in Second Reschedule REPAYMENT is NOK"); + assertEquals(10831, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT in Second Reschedule is NOK"); + assertEquals(133470, totalExpectedRepayment.intValue(), "TOTAL EXPECTED in Second Reschedule REPAYMENT is NOK"); LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); } + + @Test + public void testCreateLoanInterestGreaterThanEMIFromGapWithRecalculationEnabledAndPrincipalCompoundingOff() { + this.enableConfig(); + this.enablePrincipalCompoundingConfig(); + // create all required entities + this.createRequiredEntitiesWithRecalculationEnabledWithPrincipalCompoundingOff(); + this.createApproveLoanRescheduleRequestWithRecalculationEnabledWithPrincipalCompoundingOff(); + this.disablePrincipalCompoundingConfig(); + this.disableConfig(); + } + + private void createRequiredEntitiesWithRecalculationEnabledWithPrincipalCompoundingOff() { + this.createClientEntity(); + this.createLoanProductWithInterestRecalculation(); + this.createLoanEntityWithScheduleGapWithInterestGreaterThanEMIAndPrincipalCompoundingOff(); + } + + /** + * enables the configuration `is-principal-compounding-disabled-for-overdue-loans` + **/ + private void enablePrincipalCompoundingConfig() { + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "43", true); + } + + /** + * disables the configuration `is-principal-compounding-disabled-for-overdue-loans` + **/ + private void disablePrincipalCompoundingConfig() { + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "43", false); + } + + /** + * submit a new loan application, approve and disburse the loan + **/ + private void createLoanEntityWithScheduleGapWithInterestGreaterThanEMIAndPrincipalCompoundingOff() { + String firstRepaymentDate = "01 January 2015"; + + LOG.info("---------------------------------NEW LOAN APPLICATION------------------------------------------"); + + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("15000").withLoanTermFrequency("24") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("24").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestCalculationPeriodTypeAsDays() + .withInterestRatePerPeriod("25").withInterestTypeAsDecliningBalance().withSubmittedOnDate(this.dateString) + .withExpectedDisbursementDate(this.dateString).withFirstRepaymentDate(firstRepaymentDate) + .withinterestChargedFromDate(this.dateString).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(); + } + + /** + * create new loan reschedule request with recalculation enabled in Loan product + **/ + + private void createApproveLoanRescheduleRequestWithRecalculationEnabledWithPrincipalCompoundingOff() { + LOG.info( + "---------------------------------CREATING LOAN RESCHEDULE REQUEST FOR LOAN WITH RECALCULATION------------------------------------"); + + final String requestJSON = new LoanRescheduleRequestTestBuilder().updateGraceOnPrincipal(null).updateGraceOnInterest(null) + .updateExtraTerms(null).updateRescheduleFromDate("01 March 2015").updateAdjustedDueDate("01 July 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(798, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); + assertEquals(22567, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK"); + + LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); + + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java index 9ff751e48..a58551a9f 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java @@ -90,8 +90,8 @@ public class GlobalConfigurationHelper { ArrayList actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec); // There are currently 37 global configurations. - Assertions.assertEquals(37, expectedGlobalConfigurations.size()); - Assertions.assertEquals(37, actualGlobalConfigurations.size()); + Assertions.assertEquals(38, expectedGlobalConfigurations.size()); + Assertions.assertEquals(38, actualGlobalConfigurations.size()); for (int i = 0; i < expectedGlobalConfigurations.size(); i++) { @@ -355,14 +355,6 @@ 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); - HashMap isAccountMappedForPayment = new HashMap<>(); isAccountMappedForPayment.put("id", 35); isAccountMappedForPayment.put("name", "account-mapping-for-payment-type"); @@ -423,6 +415,22 @@ public class GlobalConfigurationHelper { isAccountNumberRandomGenerated.put("trapDoor", false); defaults.add(isAccountNumberRandomGenerated); + HashMap isInterestAppropriationEnabled = new HashMap<>(); + isInterestAppropriationEnabled.put("id", 42); + isInterestAppropriationEnabled.put("name", "is-interest-to-be-recovered-first-when-greater-than-emi"); + isInterestAppropriationEnabled.put("value", 0); + isInterestAppropriationEnabled.put("enabled", false); + isInterestAppropriationEnabled.put("trapDoor", false); + defaults.add(isInterestAppropriationEnabled); + + HashMap isPrincipalCompoundingDisabled = new HashMap<>(); + isPrincipalCompoundingDisabled.put("id", 43); + isPrincipalCompoundingDisabled.put("name", "is-principal-compounding-disabled-for-overdue-loans"); + isPrincipalCompoundingDisabled.put("value", 0); + isPrincipalCompoundingDisabled.put("enabled", false); + isPrincipalCompoundingDisabled.put("trapDoor", false); + defaults.add(isPrincipalCompoundingDisabled); + return defaults; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java index a0d17f85e..06fb68acf 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java @@ -77,6 +77,7 @@ public class LoanApplicationTestBuilder { private List> datatables = null; private List> approvalFormData = null; private String fixedPrincipalPercentagePerInstallment; + private String interestChargedFromDate; public String build(final String clientID, final String groupID, final String loanProductId, final String savingsID) { final HashMap map = new HashMap<>(); @@ -150,6 +151,7 @@ public class LoanApplicationTestBuilder { map.put("submittedOnDate", this.submittedOnDate); map.put("loanType", this.loanType); map.put("collateral", this.collaterals); + map.put("interestChargedFromDate", this.interestChargedFromDate); if (repaymentsStartingFromDate != null) { map.put("repaymentsStartingFromDate", this.repaymentsStartingFromDate); @@ -382,4 +384,9 @@ public class LoanApplicationTestBuilder { this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment; return this; } + + public LoanApplicationTestBuilder withinterestChargedFromDate(String interestChargedFromDate) { + this.interestChargedFromDate = interestChargedFromDate; + return this; + } }