FINERACT-1443-loan-interest-recover-policy-fix-when-interest-more-than-EMI (#2033)

This commit is contained in:
Manoj
2022-02-18 23:52:51 +05:30
committed by GitHub
parent fbdceea384
commit 7c235c583b
16 changed files with 310 additions and 148 deletions
@@ -75,6 +75,10 @@ public interface ConfigurationDomainService {
boolean isSkippingMeetingOnFirstDayOfMonthEnabled(); boolean isSkippingMeetingOnFirstDayOfMonthEnabled();
boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI();
boolean isPrincipalCompoundingDisabledForOverdueLoans();
Long retreivePeroidInNumberOfDaysForSkipMeetingDate(); Long retreivePeroidInNumberOfDaysForSkipMeetingDate();
boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled(); boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled();
@@ -97,8 +101,6 @@ public interface ConfigurationDomainService {
boolean isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); boolean isFirstRepaymentDateAfterRescheduleAllowedOnHoliday();
boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI();
String getAccountMappingForPaymentType(); String getAccountMappingForPaymentType();
String getAccountMappingForCharge(); String getAccountMappingForCharge();
@@ -259,8 +259,13 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
} }
@Override @Override
public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI() { public boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI() {
return getGlobalConfigurationPropertyData("is-interest-to-be-appropriated-equally-when-greater-than-emi").isEnabled(); 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 @Override
@@ -44,7 +44,8 @@ public class ScheduleGeneratorDTO {
final boolean isSkipRepaymentOnFirstDayofMonth; final boolean isSkipRepaymentOnFirstDayofMonth;
final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled;
final boolean isFirstRepaymentDateAllowedOnHoliday; final boolean isFirstRepaymentDateAllowedOnHoliday;
final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI;
final boolean isPrincipalCompoundingDisabledForOverdueLoans;
public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency applicationCurrency, public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final ApplicationCurrency applicationCurrency,
final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO, final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO,
@@ -53,7 +54,8 @@ public class ScheduleGeneratorDTO {
final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper,
final Boolean isInterestChargedFromDateAsDisbursementDateEnabled, final Integer numberOfdays, final Boolean isInterestChargedFromDateAsDisbursementDateEnabled, final Integer numberOfdays,
final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled,
final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI) { final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
final boolean isPrincipalCompoundingDisabledForOverdueLoans) {
this.loanScheduleFactory = loanScheduleFactory; this.loanScheduleFactory = loanScheduleFactory;
this.applicationCurrency = applicationCurrency; this.applicationCurrency = applicationCurrency;
@@ -71,7 +73,8 @@ public class ScheduleGeneratorDTO {
this.isSkipRepaymentOnFirstDayofMonth = isSkipRepaymentOnFirstDayofMonth; this.isSkipRepaymentOnFirstDayofMonth = isSkipRepaymentOnFirstDayofMonth;
this.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled; this.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled;
this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday;
this.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; this.isInterestToBeRecoveredFirstWhenGreaterThanEMI = isInterestToBeRecoveredFirstWhenGreaterThanEMI;
this.isPrincipalCompoundingDisabledForOverdueLoans = isPrincipalCompoundingDisabledForOverdueLoans;
} }
public LoanScheduleGeneratorFactory getLoanScheduleFactory() { public LoanScheduleGeneratorFactory getLoanScheduleFactory() {
@@ -150,7 +153,11 @@ public class ScheduleGeneratorDTO {
return isFirstRepaymentDateAllowedOnHoliday; return isFirstRepaymentDateAllowedOnHoliday;
} }
public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI() { public boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI() {
return isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; return isInterestToBeRecoveredFirstWhenGreaterThanEMI;
}
public boolean isPrincipalCompoundingDisabledForOverdueLoans() {
return isPrincipalCompoundingDisabledForOverdueLoans;
} }
} }
@@ -5623,7 +5623,8 @@ public class Loan extends AbstractPersistableCustom {
rescheduleStrategyMethod, calendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, rescheduleStrategyMethod, calendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), calendarHistoryDataWrapper, scheduleGeneratorDTO.getNumberOfdays(), scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(),
holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(), holidayDetailDTO, allowCompoundingOnEod, scheduleGeneratorDTO.isFirstRepaymentDateAllowedOnHoliday(),
scheduleGeneratorDTO.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI(), this.fixedPrincipalPercentagePerInstallment); scheduleGeneratorDTO.isInterestToBeRecoveredFirstWhenGreaterThanEMI(), this.fixedPrincipalPercentagePerInstallment,
scheduleGeneratorDTO.isPrincipalCompoundingDisabledForOverdueLoans());
return loanApplicationTerms; return loanApplicationTerms;
} }
@@ -5905,7 +5906,7 @@ public class Loan extends AbstractPersistableCustom {
compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(), compoundingCalendarInstance, compoundingFrequencyType, this.loanProduct.preCloseInterestCalculationStrategy(),
rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations, rescheduleStrategyMethod, loanCalendar, getApprovedPrincipal(), annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false, calendarHistoryDataWrapper, numberofdays, isSkipRepaymentonmonthFirst, holidayDetailDTO, allowCompoundingOnEod, false,
false, this.fixedPrincipalPercentagePerInstallment); false, this.fixedPrincipalPercentagePerInstallment, false);
} }
/** /**
@@ -286,8 +286,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// will check for EMI amount greater than interest calculated // will check for EMI amount greater than interest calculated
if (loanApplicationTerms.getFixedEmiAmount() != null if (loanApplicationTerms.getFixedEmiAmount() != null
&& loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) < 0 && loanApplicationTerms.getFixedEmiAmount().compareTo(principalInterestForThisPeriod.interest().getAmount()) < 0) {
&& !loanApplicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled()) {
String errorMsg = "EMI amount must be greater than : " + principalInterestForThisPeriod.interest().getAmount(); String errorMsg = "EMI amount must be greater than : " + principalInterestForThisPeriod.interest().getAmount();
throw new MultiDisbursementEmiAmountException(errorMsg, principalInterestForThisPeriod.interest().getAmount(), throw new MultiDisbursementEmiAmountException(errorMsg, principalInterestForThisPeriod.interest().getAmount(),
loanApplicationTerms.getFixedEmiAmount()); loanApplicationTerms.getFixedEmiAmount());
@@ -333,7 +332,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(), scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(),
currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(), currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(),
totalInstallmentDue, !isCompletePeriod); totalInstallmentDue, !isCompletePeriod);
if (principalInterestForThisPeriod.getRescheduleInterestPortion() != null) {
installment.setRescheduleInterestPortion(principalInterestForThisPeriod.getRescheduleInterestPortion().getAmount());
}
addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment); addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment);
// apply loan transactions on installments to identify early/late // apply loan transactions on installments to identify early/late
// payments for interest recalculation // payments for interest recalculation
@@ -379,44 +380,6 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(Money.zero(currency)); scheduleParams.setTotalOutstandingInterestPaymentDueToGrace(Money.zero(currency));
} }
if (loanApplicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled()
&& loanApplicationTerms.getInterestTobeApproppriated() != null
&& loanApplicationTerms.getInterestTobeApproppriated().isGreaterThanZero()) {
int emisTobeChanged = 1;
for (LoanScheduleModelPeriod installment : (List<LoanScheduleModelPeriod>) 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<LoanScheduleModelPeriod>) periods) {
if (!installment.isEMIFixedSpecificToInstallment()) {
installment.addInterestAmount(interestFraction);
installment.setRescheduleInterestPortion(interestFraction.getAmount());
interestTobeApproppriated = interestTobeApproppriated.minus(interestFraction);
}
}
LoanScheduleModelPeriod lastInstallment = ((List<LoanScheduleModelPeriod>) 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 // determine fees and penalties for charges which depends on total
// loan interest // loan interest
updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges); updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges);
@@ -1054,6 +1017,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
modifiedScheduledDueDate = loanTermVariationsData.getDateValue(); modifiedScheduledDueDate = loanTermVariationsData.getDateValue();
if (!loanTermVariationsData.isSpecificToInstallment()) { if (!loanTermVariationsData.isSpecificToInstallment()) {
scheduleParams.setActualRepaymentDate(modifiedScheduledDueDate); scheduleParams.setActualRepaymentDate(modifiedScheduledDueDate);
loanApplicationTerms.setNewScheduledDueDateStart(modifiedScheduledDueDate);
} }
loanTermVariationsData.setProcessed(true); loanTermVariationsData.setProcessed(true);
} }
@@ -2397,23 +2361,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
periods.clear(); periods.clear();
} }
BigDecimal rescheuleInterestPortionTobeRetained = BigDecimal.ZERO; if (retainedInstallments.size() > 0
for (LoanRepaymentScheduleInstallment retainedInstallment : retainedInstallments) { && retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion() != null) {
if (retainedInstallment.getRescheduleInterestPortion() != null) { loanApplicationTerms.setInterestTobeApproppriated(
rescheuleInterestPortionTobeRetained = rescheuleInterestPortionTobeRetained Money.of(loan.getCurrency(), retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion()));
.add(retainedInstallment.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); LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loan.charges(), holidayDetailDTO, loanScheduleParams);
for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) { for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) {
@@ -2640,8 +2592,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
final HolidayDetailDTO holidayDetailDTO) { final HolidayDetailDTO holidayDetailDTO) {
LocalDate nextScheduleDate = null; LocalDate nextScheduleDate = null;
if (loanApplicationTerms.getRecalculationFrequencyType().isSameAsRepayment()) { if (loanApplicationTerms.getRecalculationFrequencyType().isSameAsRepayment()) {
nextScheduleDate = this.scheduledDateGenerator.generateNextScheduleDateStartingFromDisburseDate(startDate, loanApplicationTerms, nextScheduleDate = this.scheduledDateGenerator.generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(startDate,
holidayDetailDTO); loanApplicationTerms, holidayDetailDTO);
} else { } else {
CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance(); CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance();
nextScheduleDate = CalendarUtils.getNextScheduleDate(calendarInstance.getCalendar(), startDate); nextScheduleDate = CalendarUtils.getNextScheduleDate(calendarInstance.getCalendar(), startDate);
@@ -113,7 +113,11 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS
compoundFee = compoundingMap.get(principal.getKey()); compoundFee = compoundingMap.get(principal.getKey());
compoundingMap.put(principal.getKey(), interestToBeCompounded.plus(compoundFee)); 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())) { if (interestRates.containsKey(principal.getKey())) {
loanApplicationTerms.updateAnnualNominalInterestRate(interestRates.get(principal.getKey())); loanApplicationTerms.updateAnnualNominalInterestRate(interestRates.get(principal.getKey()));
} }
@@ -128,42 +132,27 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS
interestForThisInstallment = interestForThisInstallment.plus(result.interest()); interestForThisInstallment = interestForThisInstallment.plus(result.interest());
cumulatingInterestDueToGrace = result.interestPaymentDueToGrace(); cumulatingInterestDueToGrace = result.interestPaymentDueToGrace();
Money interestTobeApproppriated = loanApplicationTerms.getInterestTobeApproppriated() == null if (loanApplicationTerms.isInterestToBeRecoveredFirstWhenGreaterThanEMIEnabled()
? Money.zero(interestForThisInstallment.getCurrency()) && loanApplicationTerms.isInterestTobeApproppriated()) {
: loanApplicationTerms.getInterestTobeApproppriated(); interestForThisInstallment = interestForThisInstallment.add(loanApplicationTerms.getInterestTobeApproppriated());
loanApplicationTerms.setInterestTobeApproppriated(interestForThisInstallment.zero());
if (loanApplicationTerms.getFixedEmiAmount() != null
&& loanApplicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled() && interestForThisInstallment
.isGreaterThan(Money.of(interestForThisInstallment.getCurrency(), loanApplicationTerms.getFixedEmiAmount()))) {
LocalDate actualPeriodEndDate = this.scheduledDateGenerator.generateNextRepaymentDate(interestStartDate, loanApplicationTerms,
false);
PrincipalInterest tempInterest = loanApplicationTerms.calculateTotalInterestForPeriod(calculator,
interestCalculationGraceOnRepaymentPeriodFraction, periodNumber, mc, cumulatingInterestDueToGrace,
balanceForInterestCalculation, interestStartDate, actualPeriodEndDate);
Money fixedEmi = Money.of(interestForThisInstallment.getCurrency(), loanApplicationTerms.getFixedEmiAmount());
if (tempInterest.interest().isGreaterThan(fixedEmi)) {
loanApplicationTerms
.setInterestTobeApproppriated(interestTobeApproppriated.plus(interestForThisInstallment.minus(fixedEmi)));
interestForThisInstallment = fixedEmi;
} else {
loanApplicationTerms.setInterestTobeApproppriated(
interestTobeApproppriated.plus(interestForThisInstallment.minus(tempInterest.interest())));
interestForThisInstallment = tempInterest.interest();
}
} }
cumulatingInterestDueToGrace = result.interestPaymentDueToGrace();
Money interestForPeriod = interestForThisInstallment; Money interestForPeriod = interestForThisInstallment;
if (interestForPeriod.isGreaterThanZero()) { if (interestForPeriod.isGreaterThanZero()) {
interestForPeriod = interestForPeriod.minus(cumulatingInterestPaymentDueToGrace); interestForPeriod = interestForPeriod.minus(cumulatingInterestPaymentDueToGrace);
} else { } else {
interestForPeriod = cumulatingInterestDueToGrace.minus(cumulatingInterestPaymentDueToGrace); interestForPeriod = cumulatingInterestDueToGrace.minus(cumulatingInterestPaymentDueToGrace);
} }
Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance, Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance,
periodNumber, mc, interestForPeriod); 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 // update cumulative fields for principal & interest
final Money interestBroughtFowardDueToGrace = cumulatingInterestDueToGrace; final Money interestBroughtFowardDueToGrace = cumulatingInterestDueToGrace;
@@ -173,6 +162,9 @@ public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanS
principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment, principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment,
totalCumulativePrincipalToDate, periodNumber); totalCumulativePrincipalToDate, periodNumber);
return new PrincipalInterest(principalForThisInstallment, interestForThisInstallment, interestBroughtFowardDueToGrace); PrincipalInterest principalInterest = new PrincipalInterest(principalForThisInstallment, interestForThisInstallment,
interestBroughtFowardDueToGrace);
principalInterest.setRescheduleInterestPortion(loanApplicationTerms.getInterestTobeApproppriated());
return principalInterest;
} }
} }
@@ -327,4 +327,22 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
generatedDate = adjustRepaymentDate(generatedDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate(); generatedDate = adjustRepaymentDate(generatedDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate();
return generatedDate; 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;
}
} }
@@ -188,7 +188,9 @@ public final class LoanApplicationTerms {
private final boolean isFirstRepaymentDateAllowedOnHoliday; private final boolean isFirstRepaymentDateAllowedOnHoliday;
private final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; private final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI;
private boolean isPrincipalCompoundingDisabledForOverdueLoans;
private final HolidayDetailDTO holidayDetailDTO; private final HolidayDetailDTO holidayDetailDTO;
@@ -209,6 +211,8 @@ public final class LoanApplicationTerms {
private Money interestTobeApproppriated; private Money interestTobeApproppriated;
private final BigDecimal fixedPrincipalPercentagePerInstallment; private final BigDecimal fixedPrincipalPercentagePerInstallment;
private LocalDate newScheduledDueDateStart;
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType, final PeriodFrequencyType repaymentPeriodFrequencyType, Integer nthDay, DayOfWeekType weekDayType,
@@ -230,7 +234,8 @@ public final class LoanApplicationTerms {
BigDecimal approvedAmount, List<LoanTermVariationsData> loanTermVariations, BigDecimal approvedAmount, List<LoanTermVariationsData> loanTermVariations,
Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final Integer numberOfDays, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final Integer numberOfDays,
boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod, 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 LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
@@ -245,8 +250,9 @@ public final class LoanApplicationTerms {
recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment,
installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, installmentAmountInMultiplesOf, preClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations,
calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays,
isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false,
fixedPrincipalPercentagePerInstallment); isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
isPrincipalCompoundingDisabledForOverdueLoans);
} }
@@ -273,7 +279,7 @@ public final class LoanApplicationTerms {
compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy,
rescheduleStrategyMethod, loanCalendar, approvedAmount, annualNominalInterestRate, loanTermVariations, rescheduleStrategyMethod, loanCalendar, approvedAmount, annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, false, calendarHistoryDataWrapper, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, false,
false, null); false, null, false);
} }
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
@@ -291,8 +297,8 @@ public final class LoanApplicationTerms {
BigDecimal annualNominalInterestRate, final List<LoanTermVariationsData> loanTermVariations, BigDecimal annualNominalInterestRate, final List<LoanTermVariationsData> loanTermVariations,
final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Integer numberOfDays, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, final Integer numberOfDays,
final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod, final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod,
final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI, final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
final BigDecimal fixedPrincipalPercentagePerInstallment) { final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans) {
final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments(); final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments();
final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery(); final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery();
@@ -331,7 +337,8 @@ public final class LoanApplicationTerms {
loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, calendarHistoryDataWrapper,
isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO,
allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday, allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday,
isInterestToBeAppropriatedEquallyWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment); isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment,
isPrincipalCompoundingDisabledForOverdueLoans);
} }
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency, public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
@@ -390,7 +397,7 @@ public final class LoanApplicationTerms {
recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment,
installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations, installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations,
calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays,
isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false, null); isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false, null, false);
} }
@@ -417,8 +424,8 @@ public final class LoanApplicationTerms {
applicationTerms.calendarHistoryDataWrapper, applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled, applicationTerms.calendarHistoryDataWrapper, applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled,
applicationTerms.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO, applicationTerms.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO,
applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization, applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization,
applicationTerms.isFirstRepaymentDateAllowedOnHoliday, applicationTerms.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI, applicationTerms.isFirstRepaymentDateAllowedOnHoliday, applicationTerms.isInterestToBeRecoveredFirstWhenGreaterThanEMI,
applicationTerms.fixedPrincipalPercentagePerInstallment); applicationTerms.fixedPrincipalPercentagePerInstallment, applicationTerms.isPrincipalCompoundingDisabledForOverdueLoans);
} }
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
@@ -443,7 +450,8 @@ public final class LoanApplicationTerms {
final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final CalendarHistoryDataWrapper calendarHistoryDataWrapper, Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled,
final Integer numberOfDays, final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO, final Integer numberOfDays, final boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO,
final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean allowCompoundingOnEod, final boolean isEqualAmortization, final boolean isFirstRepaymentDateAllowedOnHoliday,
final boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment) { final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI, final BigDecimal fixedPrincipalPercentagePerInstallment,
final boolean isPrincipalCompoundingDisabledForOverdueLoans) {
this.currency = currency; this.currency = currency;
this.loanTermFrequency = loanTermFrequency; this.loanTermFrequency = loanTermFrequency;
@@ -517,8 +525,9 @@ public final class LoanApplicationTerms {
this.totalPrincipalAccounted = principal.zero(); this.totalPrincipalAccounted = principal.zero();
this.isEqualAmortization = isEqualAmortization; this.isEqualAmortization = isEqualAmortization;
this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday; this.isFirstRepaymentDateAllowedOnHoliday = isFirstRepaymentDateAllowedOnHoliday;
this.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; this.isInterestToBeRecoveredFirstWhenGreaterThanEMI = isInterestToBeRecoveredFirstWhenGreaterThanEMI;
this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment; this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment;
this.isPrincipalCompoundingDisabledForOverdueLoans = isPrincipalCompoundingDisabledForOverdueLoans;
} }
public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate,
@@ -743,6 +752,10 @@ public final class LoanApplicationTerms {
return periodNumber == numberOfRepayments; return periodNumber == numberOfRepayments;
} }
public boolean isLastRepaymentPeriod(final int periodNumber) {
return periodNumber == this.actualNumberOfRepayments;
}
/** /**
* general method to calculate totalInterestDue discounting any grace settings * general method to calculate totalInterestDue discounting any grace settings
*/ */
@@ -1248,7 +1261,7 @@ public final class LoanApplicationTerms {
Money interestDue = Money.zero(outstandingBalance.getCurrency()); Money interestDue = Money.zero(outstandingBalance.getCurrency());
final BigDecimal periodicInterestRate = periodicInterestRate(calculator, mc, this.daysInMonthType, this.daysInYearType, 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()); interestDue = outstandingBalance.multiplyRetainScale(periodicInterestRate, mc.getRoundingMode());
return interestDue; return interestDue;
@@ -1334,7 +1347,6 @@ public final class LoanApplicationTerms {
private Money calculatePrincipalDueForInstallment(final int periodNumber, final Money totalDuePerInstallment, private Money calculatePrincipalDueForInstallment(final int periodNumber, final Money totalDuePerInstallment,
final Money periodInterest) { final Money periodInterest) {
Money principal = totalDuePerInstallment.minus(periodInterest); Money principal = totalDuePerInstallment.minus(periodInterest);
if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) { if (isPrincipalGraceApplicableForThisPeriod(periodNumber)) {
principal = principal.zero(); principal = principal.zero();
@@ -1795,15 +1807,30 @@ public final class LoanApplicationTerms {
} }
public Money getInterestTobeApproppriated() { public Money getInterestTobeApproppriated() {
return interestTobeApproppriated; return interestTobeApproppriated == null ? this.principal.zero() : interestTobeApproppriated;
} }
public void setInterestTobeApproppriated(Money interestTobeApproppriated) { public void setInterestTobeApproppriated(Money interestTobeApproppriated) {
this.interestTobeApproppriated = interestTobeApproppriated; this.interestTobeApproppriated = interestTobeApproppriated;
} }
public boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMIEnabled() { public Boolean isInterestTobeApproppriated() {
return isInterestToBeAppropriatedEquallyWhenGreaterThanEMI; 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;
}
} }
@@ -25,6 +25,7 @@ public class PrincipalInterest {
private final Money principal; private final Money principal;
private final Money interest; private final Money interest;
private final Money interestPaymentDueToGrace; private final Money interestPaymentDueToGrace;
private Money rescheduleInterestPortion;
public PrincipalInterest(final Money principal, final Money interest, final Money interestPaymentDueToGrace) { public PrincipalInterest(final Money principal, final Money interest, final Money interestPaymentDueToGrace) {
this.principal = principal; this.principal = principal;
@@ -43,4 +44,12 @@ public class PrincipalInterest {
public Money interestPaymentDueToGrace() { public Money interestPaymentDueToGrace() {
return this.interestPaymentDueToGrace; return this.interestPaymentDueToGrace;
} }
public Money getRescheduleInterestPortion() {
return rescheduleInterestPortion;
}
public void setRescheduleInterestPortion(Money rescheduleInterestPortion) {
this.rescheduleInterestPortion = rescheduleInterestPortion;
}
} }
@@ -43,4 +43,7 @@ public interface ScheduledDateGenerator {
LocalDate generateNextScheduleDateStartingFromDisburseDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms, LocalDate generateNextScheduleDateStartingFromDisburseDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms,
HolidayDetailDTO holidayDetailDTO); HolidayDetailDTO holidayDetailDTO);
LocalDate generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(LocalDate lastRepaymentDate,
LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO);
} }
@@ -448,7 +448,10 @@ public class LoanScheduleAssembler {
Date.from(expectedDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), HolidayStatusType.ACTIVE.getValue()); Date.from(expectedDisbursementDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), HolidayStatusType.ACTIVE.getValue());
final WorkingDays workingDays = this.workingDaysRepository.findOne(); final WorkingDays workingDays = this.workingDaysRepository.findOne();
HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays); 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, return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod,
interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
@@ -460,7 +463,8 @@ public class LoanScheduleAssembler {
compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment, compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment,
installmentAmountInMultiplesOf, loanProduct.preCloseInterestCalculationStrategy(), calendar, BigDecimal.ZERO, installmentAmountInMultiplesOf, loanProduct.preCloseInterestCalculationStrategy(), calendar, BigDecimal.ZERO,
loanTermVariations, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipMeetingOnFirstDay, detailDTO, loanTermVariations, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays, isSkipMeetingOnFirstDay, detailDTO,
allowCompoundingOnEod, isEqualAmortization, fixedPrincipalPercentagePerInstallment); allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI,
fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans);
} }
private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery,
@@ -144,15 +144,17 @@ public class LoanUtilService {
boolean isFirstRepaymentDateAllowedOnHoliday = this.configurationDomainService boolean isFirstRepaymentDateAllowedOnHoliday = this.configurationDomainService
.isFirstRepaymentDateAfterRescheduleAllowedOnHoliday(); .isFirstRepaymentDateAfterRescheduleAllowedOnHoliday();
boolean isInterestToBeAppropriatedEquallyWhenGreaterThanEMI = this.configurationDomainService boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI = this.configurationDomainService
.isInterestToBeAppropriatedEquallyWhenGreaterThanEMI(); .isInterestToBeRecoveredFirstWhenGreaterThanEMI();
boolean isPrincipalCompoundingDisabledForOverdueLoans = this.configurationDomainService
.isPrincipalCompoundingDisabledForOverdueLoans();
ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency, ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom, calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom,
overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper, overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper,
isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth, isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth,
isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday, isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday,
isInterestToBeAppropriatedEquallyWhenGreaterThanEMI); isInterestToBeRecoveredFirstWhenGreaterThanEMI, isPrincipalCompoundingDisabledForOverdueLoans);
return scheduleGeneratorDTO; return scheduleGeneratorDTO;
} }
@@ -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');
@@ -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() { 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() { private void disableConfig() {
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "34", false); GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(this.requestSpec, this.responseSpec, "42", false);
} }
@Test @Test
public void testCreateLoanRescheduleRequestWithInterestAppropriation() { public void testCreateLoanRescheduleRequestWithInterestAppropriation() {
// create all required entities // create all required entities
this.createRequiredEntities(); this.createRequiredEntities();
this.createAndApproveLoanRescheduleRequestForInterestAppropriation(); this.createAndApproveLoanRescheduleRequestForRecoverInterestInterestFirst();
} }
/** /**
* create new loan reschedule request * create new loan reschedule request
**/ **/
private void createAndApproveLoanRescheduleRequestForInterestAppropriation() { private void createAndApproveLoanRescheduleRequestForRecoverInterestInterestFirst() {
LOG.info( LOG.info(
"---------------------------------CREATING LOAN RESCHEDULE REQUEST FOR INTEREST APPROPRIATTION-------------------------------------"); "---------------------------------CREATING LOAN RESCHEDULE REQUEST FOR INTEREST APPROPRIATTION-------------------------------------");
@@ -279,8 +279,8 @@ public class LoanRescheduleOnDecliningBalanceLoanTest {
final HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId); final HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId);
final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment"); final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment");
assertEquals(12186, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); assertEquals(10831, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK");
assertEquals(123682, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK"); assertEquals(125184, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK");
LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); 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 HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId);
final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment"); 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"); assertEquals(131512, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK");
LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId);
@@ -338,13 +338,13 @@ public class LoanRescheduleOnDecliningBalanceLoanTest {
public void testCreateLoanRescheduleRequestForInterestAppropriationAndFixedEMI() { public void testCreateLoanRescheduleRequestForInterestAppropriationAndFixedEMI() {
// create all required entities // create all required entities
this.createRequiredEntities(); 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( LOG.info(
"---------------------------------CREATING LOAN RESCHEDULE REQUEST FOR INTEREST APPROPRIATTION-------------------------------------"); "---------------------------------CREATING LOAN RESCHEDULE REQUEST FOR INTEREST APPROPRIATTION-------------------------------------");
@@ -379,8 +379,8 @@ public class LoanRescheduleOnDecliningBalanceLoanTest {
assertEquals(5000, totalFixedDueForPeriod.intValue(), "EXPECTED FIXED REPAYMENT is NOK"); assertEquals(5000, totalFixedDueForPeriod.intValue(), "EXPECTED FIXED REPAYMENT is NOK");
assertEquals(15316, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK"); assertEquals(15417, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT is NOK");
assertEquals(120806, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK"); assertEquals(121412, totalExpectedRepayment.intValue(), "TOTAL EXPECTED REPAYMENT is NOK");
LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId);
@@ -390,7 +390,7 @@ public class LoanRescheduleOnDecliningBalanceLoanTest {
public void testCreateLoanRescheduleRequestWithMultpleInterestAppropriation() { public void testCreateLoanRescheduleRequestWithMultpleInterestAppropriation() {
// create all required entities // create all required entities
this.createRequiredEntities(); this.createRequiredEntities();
this.createAndApproveLoanRescheduleRequestForInterestAppropriation(); this.createAndApproveLoanRescheduleRequestForRecoverInterestInterestFirst();
this.createAndApproveLoanRescheduleRequestForSecondInterestAppropriation(); this.createAndApproveLoanRescheduleRequestForSecondInterestAppropriation();
@@ -429,9 +429,105 @@ public class LoanRescheduleOnDecliningBalanceLoanTest {
final HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId); final HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(requestSpec, generalResponseSpec, loanId);
final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment"); final Float totalExpectedRepayment = (Float) loanSummary.get("totalExpectedRepayment");
assertEquals(12187, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT in Second Reschedule is NOK"); assertEquals(10831, totalDueForPeriod.intValue(), "EXPECTED REPAYMENT in Second Reschedule is NOK");
assertEquals(130750, totalExpectedRepayment.intValue(), "TOTAL EXPECTED in Second Reschedule REPAYMENT is NOK"); assertEquals(133470, totalExpectedRepayment.intValue(), "TOTAL EXPECTED in Second Reschedule REPAYMENT is NOK");
LOG.info("Successfully approved loan reschedule request (ID: {})", this.loanRescheduleRequestId); 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);
}
} }
@@ -90,8 +90,8 @@ public class GlobalConfigurationHelper {
ArrayList<HashMap> actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec); ArrayList<HashMap> actualGlobalConfigurations = getAllGlobalConfigurations(requestSpec, responseSpec);
// There are currently 37 global configurations. // There are currently 37 global configurations.
Assertions.assertEquals(37, expectedGlobalConfigurations.size()); Assertions.assertEquals(38, expectedGlobalConfigurations.size());
Assertions.assertEquals(37, actualGlobalConfigurations.size()); Assertions.assertEquals(38, actualGlobalConfigurations.size());
for (int i = 0; i < expectedGlobalConfigurations.size(); i++) { for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
@@ -355,14 +355,6 @@ public class GlobalConfigurationHelper {
isFirstPaydayAllowedOnHoliday.put("trapDoor", false); isFirstPaydayAllowedOnHoliday.put("trapDoor", false);
defaults.add(isFirstPaydayAllowedOnHoliday); defaults.add(isFirstPaydayAllowedOnHoliday);
HashMap<String, Object> isInterestAppropriationEnabled = new HashMap<>();
isInterestAppropriationEnabled.put("id", 34);
isInterestAppropriationEnabled.put("name", "is-interest-to-be-appropriated-equally-when-greater-than-emi");
isInterestAppropriationEnabled.put("value", 0);
isInterestAppropriationEnabled.put("enabled", false);
isInterestAppropriationEnabled.put("trapDoor", false);
defaults.add(isInterestAppropriationEnabled);
HashMap<String, Object> isAccountMappedForPayment = new HashMap<>(); HashMap<String, Object> isAccountMappedForPayment = new HashMap<>();
isAccountMappedForPayment.put("id", 35); isAccountMappedForPayment.put("id", 35);
isAccountMappedForPayment.put("name", "account-mapping-for-payment-type"); isAccountMappedForPayment.put("name", "account-mapping-for-payment-type");
@@ -423,6 +415,22 @@ public class GlobalConfigurationHelper {
isAccountNumberRandomGenerated.put("trapDoor", false); isAccountNumberRandomGenerated.put("trapDoor", false);
defaults.add(isAccountNumberRandomGenerated); defaults.add(isAccountNumberRandomGenerated);
HashMap<String, Object> 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<String, Object> 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; return defaults;
} }
@@ -77,6 +77,7 @@ public class LoanApplicationTestBuilder {
private List<HashMap<String, Object>> datatables = null; private List<HashMap<String, Object>> datatables = null;
private List<Map<String, Object>> approvalFormData = null; private List<Map<String, Object>> approvalFormData = null;
private String fixedPrincipalPercentagePerInstallment; private String fixedPrincipalPercentagePerInstallment;
private String interestChargedFromDate;
public String build(final String clientID, final String groupID, final String loanProductId, final String savingsID) { public String build(final String clientID, final String groupID, final String loanProductId, final String savingsID) {
final HashMap<String, Object> map = new HashMap<>(); final HashMap<String, Object> map = new HashMap<>();
@@ -150,6 +151,7 @@ public class LoanApplicationTestBuilder {
map.put("submittedOnDate", this.submittedOnDate); map.put("submittedOnDate", this.submittedOnDate);
map.put("loanType", this.loanType); map.put("loanType", this.loanType);
map.put("collateral", this.collaterals); map.put("collateral", this.collaterals);
map.put("interestChargedFromDate", this.interestChargedFromDate);
if (repaymentsStartingFromDate != null) { if (repaymentsStartingFromDate != null) {
map.put("repaymentsStartingFromDate", this.repaymentsStartingFromDate); map.put("repaymentsStartingFromDate", this.repaymentsStartingFromDate);
@@ -382,4 +384,9 @@ public class LoanApplicationTestBuilder {
this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment; this.fixedPrincipalPercentagePerInstallment = fixedPrincipalPercentagePerInstallment;
return this; return this;
} }
public LoanApplicationTestBuilder withinterestChargedFromDate(String interestChargedFromDate) {
this.interestChargedFromDate = interestChargedFromDate;
return this;
}
} }