diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java index 443ea7ca4..ba58b25f7 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java @@ -450,4 +450,21 @@ public final class MathUtil { } return percentageOf; } + + /** + * Calculate percentage of a value + * + * @param value + * @param percentage + * @param mc + * @return + */ + public static BigDecimal percentageOf(final BigDecimal value, final BigDecimal percentage, final MathContext mc) { + BigDecimal percentageOf = BigDecimal.ZERO; + if (isGreaterThanZero(value)) { + final BigDecimal multiplicand = percentage.divide(BigDecimal.valueOf(100L), mc); + percentageOf = value.multiply(multiplicand, mc); + } + return percentageOf; + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java index b8e2fc0fe..c7e290af6 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java @@ -129,6 +129,15 @@ public class Money implements Comparable { return amountScaled; } + public static Money roundToMultiplesOf(final Money existingVal, final Integer inMultiplesOf) { + BigDecimal amountScaled = existingVal.getAmount(); + BigDecimal inMultiplesOfValue = BigDecimal.valueOf(inMultiplesOf.intValue()); + if (inMultiplesOfValue.compareTo(BigDecimal.ZERO) > 0) { + amountScaled = amountScaled.divide(inMultiplesOfValue, 0, RoundingMode.HALF_UP).multiply(inMultiplesOfValue); + } + return Money.of(existingVal.getCurrency(), amountScaled); + } + public static double ceiling(final double n, final double s) { double c; diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/calendar/service/CalendarUtils.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/calendar/service/CalendarUtils.java index 409ffb088..b06c1afaa 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/calendar/service/CalendarUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/calendar/service/CalendarUtils.java @@ -717,15 +717,12 @@ public final class CalendarUtils { if (recur == null) { return null; } - LocalDate date = startDate; final LocalDate seedDate = calendar.getStartDateLocalDate(); /** * if (isValidRedurringDate(calendar.getRecurrence(), seedDate, date)) { date = date.plusDays(1); } **/ - final LocalDate scheduleDate = getNextRecurringDate(recur, seedDate, date); - - return scheduleDate; + return getNextRecurringDate(recur, seedDate, startDate); } public static void validateNthDayOfMonthFrequency(DataValidatorBuilder baseDataValidator, final String repeatsOnNthDayOfMonthParamName, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 15cf20fee..ea173c3d4 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1289,7 +1289,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); applyAccurals(); - } public void updateLoanSchedule(final Collection installments) { @@ -2530,9 +2529,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { LocalDate firstInstallmentDueDate = fetchRepaymentScheduleInstallment(1).getDueDate(); if ((interestRecalculationEnabledParam && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) { regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); - } else if (downPaymentEnabled) { - LoanScheduleDTO loanSchedule = getRecalculatedSchedule(scheduleGeneratorDTO); - updateLoanSchedule(loanSchedule.getInstallments()); } } @@ -2827,11 +2823,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final MathContext mc = new MathContext(8, roundingMode); final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO); - LoanScheduleGenerator loanScheduleGenerator = null; + LoanScheduleGenerator loanScheduleGenerator; if (loanApplicationTerms.isEqualAmortization()) { if (loanApplicationTerms.getInterestMethod().isDecliningBalance()) { final LoanScheduleGenerator decliningLoanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory() - .create(InterestMethod.DECLINING_BALANCE); + .create(loanApplicationTerms.getLoanScheduleType(), InterestMethod.DECLINING_BALANCE); Set loanCharges = getActiveCharges(); LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, scheduleGeneratorDTO.getHolidayDetailDTO()); @@ -2840,9 +2836,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged())); } - loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(InterestMethod.FLAT); + loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getLoanScheduleType(), + InterestMethod.FLAT); } else { - loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getInterestMethod()); + loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(loanApplicationTerms.getLoanScheduleType(), + loanApplicationTerms.getInterestMethod()); } return loanScheduleGenerator.generate(mc, loanApplicationTerms, getActiveCharges(), scheduleGeneratorDTO.getHolidayDetailDTO()); @@ -3401,8 +3399,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final List repaymentsOrWaivers = new ArrayList<>(); List trans = getLoanTransactions(); for (final LoanTransaction transaction : trans) { - if (transaction.isNotReversed() - && (transaction.isChargeOff() || !(transaction.isDisbursement() || transaction.isNonMonetaryTransaction()))) { + if (transaction.isNotReversed() && (transaction.isChargeOff() || !transaction.isNonMonetaryTransaction())) { repaymentsOrWaivers.add(transaction); } } @@ -3413,9 +3410,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public List retrieveListOfTransactionsPostDisbursementExcludeAccruals() { final List repaymentsOrWaivers = new ArrayList<>(); for (final LoanTransaction transaction : this.loanTransactions) { - if (transaction.isNotReversed() - && !(transaction.isDisbursement() || transaction.isAccrual() || transaction.isRepaymentAtDisbursement() - || transaction.isNonMonetaryTransaction() || transaction.isIncomePosting())) { + if (transaction.isNotReversed() && !(transaction.isAccrual() || transaction.isRepaymentAtDisbursement() + || transaction.isNonMonetaryTransaction() || transaction.isIncomePosting())) { repaymentsOrWaivers.add(transaction); } } @@ -5754,7 +5750,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return null; } final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); - final LoanScheduleGenerator loanScheduleGenerator = generatorDTO.getLoanScheduleFactory().create(interestMethod); + final LoanScheduleGenerator loanScheduleGenerator = generatorDTO.getLoanScheduleFactory() + .create(this.loanRepaymentScheduleDetail.getLoanScheduleType(), interestMethod); final RoundingMode roundingMode = MoneyHelper.getRoundingMode(); final MathContext mc = new MathContext(19, roundingMode); @@ -5778,7 +5775,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod(); final LoanApplicationTerms loanApplicationTerms = constructLoanApplicationTerms(scheduleGeneratorDTO); - final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory().create(interestMethod); + final LoanScheduleGenerator loanScheduleGenerator = scheduleGeneratorDTO.getLoanScheduleFactory() + .create(loanApplicationTerms.getLoanScheduleType(), interestMethod); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); installment = loanScheduleGenerator.calculatePrepaymentAmount(getCurrency(), onDate, loanApplicationTerms, mc, this, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 89f0e2587..fd7f81936 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -419,13 +419,9 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa this.penaltyAccrued = null; } - public void resetChargesFields() { + public void resetChargesCharged() { this.feeChargesCharged = null; - this.feeChargesWaived = null; - this.feeChargesWrittenOff = null; this.penaltyCharges = null; - this.penaltyChargesWaived = null; - this.penaltyChargesWrittenOff = null; } public Money payPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { @@ -656,6 +652,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.plus(this.penaltyCharges).getAmount()); this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.plus(this.penaltyChargesWaived).getAmount()); this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.plus(this.penaltyChargesWrittenOff).getAmount()); + checkIfRepaymentPeriodObligationsAreMet(getObligationsMetOnDate(), feeChargesDue.getCurrency()); } public void updateAccrualPortion(final Money interest, final Money feeCharges, final Money penalityCharges) { @@ -961,4 +958,13 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa return isDownPayment; } + public void resetBalances() { + resetDerivedComponents(); + resetPrincipalDue(); + resetChargesCharged(); + } + + public void resetPrincipalDue() { + this.principal = null; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index e741b003d..ce20a975a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -218,6 +218,7 @@ public final class LoanApplicationTerms { private LocalDate submittedOnDate; private boolean isScheduleExtensionForDownPaymentDisabled; private Money disbursedPrincipal; + private final LoanScheduleType loanScheduleType; public static LoanApplicationTerms assembleFrom(final ApplicationCurrency currency, final Integer loanTermFrequency, final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery, @@ -244,7 +245,8 @@ public final class LoanApplicationTerms { final BigDecimal fixedPrincipalPercentagePerInstallment, final boolean isPrincipalCompoundingDisabledForOverdueLoans, final Boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType, - final LocalDate submittedOnDate, final Boolean isScheduleExtensionForDownPaymentDisabled) { + final LocalDate submittedOnDate, final Boolean isScheduleExtensionForDownPaymentDisabled, + final LoanScheduleType loanScheduleType) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; @@ -262,7 +264,8 @@ public final class LoanApplicationTerms { isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment, - isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled); + isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled, + loanScheduleType); } @@ -318,6 +321,7 @@ public final class LoanApplicationTerms { isAutoRepaymentForDownPaymentEnabled = loanProductRelatedDetail.isEnableAutoRepaymentForDownPayment(); isScheduleExtensionForDownPaymentDisabled = loanProductRelatedDetail.isDisableScheduleExtensionForDownPayment(); } + LoanScheduleType loanScheduleType = loanProductRelatedDetail.getLoanScheduleType(); return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, ((nthDay != null) ? nthDay.getValue() : null), dayOfWeek, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, @@ -333,7 +337,8 @@ public final class LoanApplicationTerms { allowCompoundingOnEod, isEqualAmortization, isFirstRepaymentDateAllowedOnHoliday, isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment, - isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled); + isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, isScheduleExtensionForDownPaymentDisabled, + loanScheduleType); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -362,7 +367,7 @@ public final class LoanApplicationTerms { final boolean isPrincipalCompoundingDisabledForOverdueLoans, final boolean isDownPaymentEnabled, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, - final boolean isScheduleExtensionForDownPaymentDisabled) { + final boolean isScheduleExtensionForDownPaymentDisabled, final LoanScheduleType loanScheduleType) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -446,6 +451,7 @@ public final class LoanApplicationTerms { this.repaymentStartDateType = repaymentStartDateType; this.submittedOnDate = submittedOnDate; this.isScheduleExtensionForDownPaymentDisabled = isScheduleExtensionForDownPaymentDisabled; + this.loanScheduleType = loanScheduleType; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -600,7 +606,7 @@ public final class LoanApplicationTerms { } public PrincipalInterest calculateTotalInterestForPeriod(final PaymentPeriodsInOneYearCalculator calculator, - final double interestCalculationGraceOnRepaymentPeriodFraction, final int periodNumber, final MathContext mc, + final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final int periodNumber, final MathContext mc, final Money cumulatingInterestPaymentDueToGrace, final Money outstandingBalance, final LocalDate periodStartDate, final LocalDate periodEndDate) { @@ -653,8 +659,6 @@ public final class LoanApplicationTerms { interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.zero(); } else if (isInterestFreeGracePeriod(periodNumber)) { interestForInstallment = interestForInstallment.zero(); - } else if (isInterestFreeGracePeriodFromDate(interestCalculationGraceOnRepaymentPeriodFraction)) { - interestForInstallment = interestForThisInstallmentAfterGrace; } else { interestBroughtForwardDueToGrace = interestBroughtForwardDueToGrace.plus(interestForThisInstallmentBeforeGrace); } @@ -1185,7 +1189,7 @@ public final class LoanApplicationTerms { } private Money calculateDecliningInterestDueForInstallmentAfterApplyingGrace(final PaymentPeriodsInOneYearCalculator calculator, - final double interestCalculationGraceOnRepaymentPeriodFraction, final MathContext mc, final Money outstandingBalance, + final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final MathContext mc, final Money outstandingBalance, final int periodNumber, LocalDate periodStartDate, LocalDate periodEndDate) { Money interest = calculateDecliningInterestDueForInstallmentBeforeApplyingGrace(calculator, mc, outstandingBalance, periodStartDate, @@ -1195,30 +1199,30 @@ public final class LoanApplicationTerms { interest = interest.zero(); } - Double fraction = interestCalculationGraceOnRepaymentPeriodFraction; + BigDecimal fraction = interestCalculationGraceOnRepaymentPeriodFraction; if (isInterestFreeGracePeriod(periodNumber)) { interest = interest.zero(); } else if (isInterestFreeGracePeriodFromDate(interestCalculationGraceOnRepaymentPeriodFraction)) { - if (interestCalculationGraceOnRepaymentPeriodFraction >= Integer.valueOf(1).doubleValue()) { + if (interestCalculationGraceOnRepaymentPeriodFraction.compareTo(BigDecimal.ZERO) > 0) { interest = interest.zero(); - fraction = fraction - Integer.valueOf(1).doubleValue(); + fraction = fraction.subtract(BigDecimal.ONE); - } else if (interestCalculationGraceOnRepaymentPeriodFraction > Double.parseDouble("0.25") - && interestCalculationGraceOnRepaymentPeriodFraction < Integer.valueOf(1).doubleValue()) { + } else if (interestCalculationGraceOnRepaymentPeriodFraction.compareTo(BigDecimal.valueOf(0.25)) > 0 + && interestCalculationGraceOnRepaymentPeriodFraction.compareTo(BigDecimal.ONE) < 0) { final Money graceOnInterestForRepaymentPeriod = interest.multipliedBy(interestCalculationGraceOnRepaymentPeriodFraction); interest = interest.minus(graceOnInterestForRepaymentPeriod); - fraction = Double.valueOf("0"); + fraction = BigDecimal.ZERO; } } return interest; } - private boolean isInterestFreeGracePeriodFromDate(final double interestCalculationGraceOnRepaymentPeriodFraction) { - return this.interestChargedFromDate != null && interestCalculationGraceOnRepaymentPeriodFraction > Double.parseDouble("0.0"); + private boolean isInterestFreeGracePeriodFromDate(BigDecimal interestCalculationGraceOnRepaymentPeriodFraction) { + return this.interestChargedFromDate != null && interestCalculationGraceOnRepaymentPeriodFraction.compareTo(BigDecimal.ZERO) > 0; } private Money calculateEqualPrincipalDueForInstallment(final MathContext mc, final int periodNumber) { @@ -1291,7 +1295,7 @@ public final class LoanApplicationTerms { this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled, this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled, - this.isScheduleExtensionForDownPaymentDisabled); + this.isScheduleExtensionForDownPaymentDisabled, this.loanScheduleType); } public Integer getLoanTermFrequency() { @@ -1322,6 +1326,10 @@ public final class LoanApplicationTerms { this.principal = principal; } + public void setDisbursedPrincipal(Money disbursedPrincipal) { + this.disbursedPrincipal = disbursedPrincipal; + } + public LocalDate getInterestChargedFromLocalDate() { return this.interestChargedFromDate; } @@ -1781,4 +1789,8 @@ public final class LoanApplicationTerms { public Integer getInstallmentAmountInMultiplesOf() { return installmentAmountInMultiplesOf; } + + public LoanScheduleType getLoanScheduleType() { + return loanScheduleType; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java index 8ce768f21..46594b09e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorFactory.java @@ -22,5 +22,5 @@ import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; public interface LoanScheduleGeneratorFactory { - LoanScheduleGenerator create(InterestMethod interestMethod); + LoanScheduleGenerator create(LoanScheduleType loanScheduleType, InterestMethod interestMethod); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java index e7e8d92c2..9b36bce95 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModel.java @@ -20,7 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collection; +import java.util.List; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -32,7 +32,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP */ public final class LoanScheduleModel { - private final Collection periods; + private final List periods; private final ApplicationCurrency applicationCurrency; private final int loanTermInDays; private final Money totalPrincipalDisbursed; @@ -44,7 +44,7 @@ public final class LoanScheduleModel { private final BigDecimal totalRepaymentExpected; private final BigDecimal totalOutstanding; - public static LoanScheduleModel from(final Collection periods, final ApplicationCurrency applicationCurrency, + public static LoanScheduleModel from(final List periods, final ApplicationCurrency applicationCurrency, final int loanTermInDays, final Money principalDisbursed, final BigDecimal totalPrincipalExpected, final BigDecimal totalPrincipalPaid, final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, final BigDecimal totalPenaltyChargesCharged, final BigDecimal totalRepaymentExpected, final BigDecimal totalOutstanding) { @@ -54,7 +54,7 @@ public final class LoanScheduleModel { totalOutstanding); } - public static LoanScheduleModel withOverdueChargeUpdation(final Collection periods, + public static LoanScheduleModel withOverdueChargeUpdation(final List periods, final LoanScheduleModel loanScheduleModel, final BigDecimal totalPenaltyChargesCharged) { return new LoanScheduleModel(periods, loanScheduleModel.applicationCurrency, loanScheduleModel.loanTermInDays, @@ -63,7 +63,7 @@ public final class LoanScheduleModel { loanScheduleModel.totalRepaymentExpected, loanScheduleModel.totalOutstanding); } - public static LoanScheduleModel withLoanScheduleModelPeriods(final Collection periods, + public static LoanScheduleModel withLoanScheduleModelPeriods(final List periods, final LoanScheduleModel loanScheduleModel) { return new LoanScheduleModel(periods, loanScheduleModel.applicationCurrency, loanScheduleModel.loanTermInDays, @@ -72,7 +72,7 @@ public final class LoanScheduleModel { loanScheduleModel.totalPenaltyChargesCharged, loanScheduleModel.totalRepaymentExpected, loanScheduleModel.totalOutstanding); } - private LoanScheduleModel(final Collection periods, final ApplicationCurrency applicationCurrency, + private LoanScheduleModel(final List periods, final ApplicationCurrency applicationCurrency, final int loanTermInDays, final Money principalDisbursed, final BigDecimal totalPrincipalExpected, final BigDecimal totalPrincipalPaid, final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged, final BigDecimal totalPenaltyChargesCharged, final BigDecimal totalRepaymentExpected, final BigDecimal totalOutstanding) { @@ -97,7 +97,7 @@ public final class LoanScheduleModel { final BigDecimal totalCredits = BigDecimal.ZERO; - final Collection periodsData = new ArrayList<>(); + final List periodsData = new ArrayList<>(); for (final LoanScheduleModelPeriod modelPeriod : this.periods) { periodsData.add(modelPeriod.toData()); } @@ -114,7 +114,7 @@ public final class LoanScheduleModel { totalPaidInAdvance, totalPaidLate, this.totalOutstanding, totalCredits); } - public Collection getPeriods() { + public List getPeriods() { return this.periods; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleType.java new file mode 100644 index 000000000..cf6d574b5 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleType.java @@ -0,0 +1,23 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; + +public enum LoanScheduleType { + CUMULATIVE, PROGRESSIVE +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java index 30b08725f..508c6379f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PaymentPeriodsInOneYearCalculator.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import java.math.BigDecimal; +import java.math.MathContext; import java.time.LocalDate; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -25,7 +27,7 @@ public interface PaymentPeriodsInOneYearCalculator { Integer calculate(PeriodFrequencyType repaymentFrequencyType); - double calculatePortionOfRepaymentPeriodInterestChargingGrace(LocalDate repaymentPeriodStartDate, LocalDate scheduledDueDate, - LocalDate interestChargedFromLocalDate, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaidEvery); + BigDecimal calculatePortionOfRepaymentPeriodInterestChargingGrace(LocalDate repaymentPeriodStartDate, LocalDate scheduledDueDate, + LocalDate interestChargedFromLocalDate, PeriodFrequencyType repaymentPeriodFrequencyType, int repaidEvery, MathContext mc); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index ee1f6ad42..c58e7cb50 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -156,4 +156,7 @@ public interface LoanProductConstants { String ENABLE_INSTALLMENT_LEVEL_DELINQUENCY = "enableInstallmentLevelDelinquency"; + // loan schedule type + String LOAN_SCHEDULE_TYPE = "loanScheduleType"; + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index 66c2dc171..c18c77409 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -62,6 +62,7 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData; import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate; import org.apache.fineract.portfolio.fund.domain.Fund; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException; import org.apache.fineract.portfolio.rate.domain.Rate; @@ -303,6 +304,14 @@ public class LoanProduct extends AbstractPersistableCustom { final LocalDate closeDate = command.localDateValueOfParameterNamed("closeDate"); final ExternalId externalId = ExternalIdFactory.produce(command.stringValueOfParameterNamedAllowingNull("externalId")); + final LoanScheduleType loanScheduleType; + if (command.hasParameter("loanScheduleType")) { + loanScheduleType = LoanScheduleType.valueOf(command.stringValueOfParameterNamed("loanScheduleType")); + } else { + // For backward compatibility + loanScheduleType = LoanScheduleType.CUMULATIVE; + } + final boolean useBorrowerCycle = command .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.USE_BORROWER_CYCLE_PARAMETER_NAME); final Set loanProductBorrowerCycleVariations = new HashSet<>(); @@ -427,7 +436,7 @@ public class LoanProduct extends AbstractPersistableCustom { disallowExpectedDisbursements, allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, repaymentStartDateType, disableScheduleExtensionForDownPayment, - enableInstallmentLevelDelinquency); + enableInstallmentLevelDelinquency, loanScheduleType); } @@ -642,7 +651,8 @@ public class LoanProduct extends AbstractPersistableCustom { final Integer overAppliedNumber, final Integer dueDaysForRepaymentEvent, final Integer overDueDaysForRepaymentEvent, final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final RepaymentStartDateType repaymentStartDateType, - final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency) { + final boolean disableScheduleExtensionForDownPayment, final boolean enableInstallmentLevelDelinquency, + final LoanScheduleType loanScheduleType) { this.fund = fund; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; @@ -684,7 +694,7 @@ public class LoanProduct extends AbstractPersistableCustom { recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, - enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment); + enableAutoRepaymentForDownPayment, disableScheduleExtensionForDownPayment, loanScheduleType); this.loanProductRelatedDetail.validateRepaymentPeriodWithGraceSettings(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index b255ff9ea..186d163d2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -39,6 +39,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; /** @@ -141,6 +142,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "disable_schedule_extension_for_down_payment", nullable = false) private boolean disableScheduleExtensionForDownPayment; + @Column(name = "loan_schedule_type", nullable = false) + @Enumerated(EnumType.STRING) + private LoanScheduleType loanScheduleType; + public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, @@ -151,7 +156,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final BigDecimal inArrearsTolerance, final Integer graceOnArrearsAgeing, final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization, final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, - final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment) { + final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment, + final LoanScheduleType loanScheduleType) { return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, @@ -159,7 +165,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, - disableScheduleExtensionForDownPayment); + disableScheduleExtensionForDownPayment, loanScheduleType); } protected LoanProductRelatedDetail() { @@ -176,7 +182,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final BigDecimal inArrearsTolerance, final Integer graceOnArrearsAgeing, final Integer daysInMonthType, final Integer daysInYearType, final boolean isInterestRecalculationEnabled, final boolean isEqualAmortization, final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, - final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment) { + final boolean enableAutoRepaymentForDownPayment, final boolean disableScheduleExtensionForDownPayment, + final LoanScheduleType loanScheduleType) { this.currency = currency; this.principal = defaultPrincipal; this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; @@ -207,6 +214,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.disbursedAmountPercentageForDownPayment = disbursedAmountPercentageForDownPayment; this.enableAutoRepaymentForDownPayment = enableAutoRepaymentForDownPayment; this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment; + this.loanScheduleType = loanScheduleType; } private Integer defaultToNullIfZero(final Integer value) { @@ -729,4 +737,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.disableScheduleExtensionForDownPayment = disableScheduleExtensionForDownPayment; } + public LoanScheduleType getLoanScheduleType() { + return loanScheduleType; + } } diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml index 56a18e16e..5104d0784 100644 --- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml @@ -32,4 +32,5 @@ + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1010_introduce_loan_schedule_type_configuration.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1010_introduce_loan_schedule_type_configuration.xml new file mode 100644 index 000000000..9bae4b0df --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1010_introduce_loan_schedule_type_configuration.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 0f71ee189..feaac762c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; import java.math.BigDecimal; +import java.math.MathContext; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; @@ -31,6 +32,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.ObjectUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; @@ -43,6 +45,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepayme import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.jetbrains.annotations.NotNull; @@ -144,8 +147,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { - currentInstallment.resetDerivedComponents(); - currentInstallment.resetChargesFields(); + currentInstallment.resetBalances(); currentInstallment.updateDerivedFields(currency, disbursementDate); } 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/AbstractCumulativeLoanScheduleGenerator.java similarity index 90% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java index 375b7c51c..236ecb7ed 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/AbstractCumulativeLoanScheduleGenerator.java @@ -59,16 +59,12 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDis import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.ScheduleDateException; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; -public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGenerator { - - protected final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); - private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator = new DefaultPaymentPeriodsInOneYearCalculator(); +public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanScheduleGenerator { @Override public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final Set loanCharges, final HolidayDetailDTO holidayDetailDTO) { - final LoanScheduleParams loanScheduleRecalculationDTO = null; - return generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, loanScheduleRecalculationDTO); + return generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, null); } private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, @@ -76,7 +72,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency(); // generate list of proposed schedule due dates - LocalDate loanEndDate = this.scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); + LocalDate loanEndDate = getScheduledDateGenerator().getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations() .fetchLoanTermDueDateVariationsData(loanEndDate); if (lastDueDateVariation != null) { @@ -91,8 +87,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // schedule generation. final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); - final int numberOfRepayments = loanApplicationTerms.fetchNumberOfRepaymentsAfterExceptions(); - LoanScheduleParams scheduleParams = null; + LoanScheduleParams scheduleParams; LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType()) ? loanApplicationTerms.getExpectedDisbursementDate() : loanApplicationTerms.getSubmittedOnDate(); @@ -110,7 +105,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = scheduleParams .getLoanRepaymentScheduleTransactionProcessor(); - Collection periods = new ArrayList<>(); + List periods = new ArrayList<>(); if (!scheduleParams.isPartialUpdate()) { periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, scheduleParams, chargesDueAtTimeOfDisbursement); @@ -120,7 +115,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // interest method . if (!scheduleParams.isPartialUpdate() && !loanApplicationTerms.isEqualAmortization()) { Money totalInterestChargedForFullLoanTerm = loanApplicationTerms - .calculateTotalInterestCharged(this.paymentPeriodsInOneYearCalculator, mc); + .calculateTotalInterestCharged(getPaymentPeriodsInOneYearCalculator(), mc); loanApplicationTerms.updateTotalInterestDue(totalInterestChargedForFullLoanTerm); @@ -130,10 +125,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener LocalDate lastRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType()) ? loanApplicationTerms.getExpectedDisbursementDate() : loanApplicationTerms.getSubmittedOnDate(); - LocalDate firstRepaymentdate = this.scheduledDateGenerator.generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, + LocalDate firstRepaymentDate = getScheduledDateGenerator().generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment); - final LocalDate idealDisbursementDate = this.scheduledDateGenerator.idealDisbursementDateBasedOnFirstRepaymentDate( - loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), firstRepaymentdate, + final LocalDate idealDisbursementDate = getScheduledDateGenerator().idealDisbursementDateBasedOnFirstRepaymentDate( + loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getRepaymentEvery(), firstRepaymentDate, loanApplicationTerms.getLoanCalendar(), loanApplicationTerms.getHolidayDetailDTO(), loanApplicationTerms); if (!scheduleParams.isPartialUpdate()) { @@ -148,7 +143,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } Money calculatedAmortizableAmount = loanApplicationTerms.getPrincipal().minus(downPaymentAmount); - scheduleParams.setOutstandingBalance(calculatedAmortizableAmount); + scheduleParams.reduceOutstandingBalance(Money.of(currency, downPaymentAmount)); // Set Fixed Principal Amount updateAmortization(mc, loanApplicationTerms, scheduleParams.getPeriodNumber(), calculatedAmortizableAmount); @@ -174,7 +169,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // charges which depends on total loan interest will be added to this // set and handled separately after all installments generated - final Set nonCompoundingCharges = seperateTotalCompoundingPercentageCharges(loanCharges); + final Set nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges); LocalDate currentDate = DateUtils.getBusinessLocalDate(); LocalDate lastRestDate = currentDate; @@ -183,9 +178,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } boolean isNextRepaymentAvailable = true; - Boolean extendTermForDailyRepayments = false; + boolean extendTermForDailyRepayments = false; - if (holidayDetailDTO.getWorkingDays().getExtendTermForDailyRepayments() == true + if (holidayDetailDTO.getWorkingDays().getExtendTermForDailyRepayments() && loanApplicationTerms.getRepaymentPeriodFrequencyType() == PeriodFrequencyType.DAYS && loanApplicationTerms.getRepaymentEvery() == 1) { holidayDetailDTO.getWorkingDays().setRepaymentReschedulingType(RepaymentRescheduleType.MOVE_TO_NEXT_WORKING_DAY.getValue()); @@ -204,16 +199,16 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener applyLoanVariationsForPartialScheduleGenerate(loanApplicationTerms, scheduleParams, interestRates, interestRatesForInstallments); - if (!DateUtils.isAfter(firstRepaymentdate, scheduleParams.getActualRepaymentDate())) { + if (!DateUtils.isAfter(firstRepaymentDate, scheduleParams.getActualRepaymentDate())) { isFirstRepayment = false; } } while (!scheduleParams.getOutstandingBalance().isZero() || !scheduleParams.getDisburseDetailMap().isEmpty()) { LocalDate previousRepaymentDate = scheduleParams.getActualRepaymentDate(); - scheduleParams.setActualRepaymentDate(this.scheduledDateGenerator + scheduleParams.setActualRepaymentDate(getScheduledDateGenerator() .generateNextRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment)); - AdjustedDateDetailsDTO adjustedDateDetailsDTO = this.scheduledDateGenerator + AdjustedDateDetailsDTO adjustedDateDetailsDTO = getScheduledDateGenerator() .adjustRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, holidayDetailDTO); scheduleParams.setActualRepaymentDate(adjustedDateDetailsDTO.getChangedActualRepaymentDate()); isFirstRepayment = false; @@ -221,17 +216,17 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // calculated interest start date for the period LocalDate periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, - scheduleParams.getPeriodStartDate(), idealDisbursementDate, firstRepaymentdate, + scheduleParams.getPeriodStartDate(), idealDisbursementDate, firstRepaymentDate, loanApplicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled(), loanApplicationTerms.getExpectedDisbursementDate()); // Loan Schedule Exceptions that need to be applied for Loan Account LoanTermVariationParams termVariationParams = applyLoanTermVariations(loanApplicationTerms, scheduleParams, - previousRepaymentDate, scheduledDueDate, interestRatesForInstallments, this.paymentPeriodsInOneYearCalculator, mc); + previousRepaymentDate, scheduledDueDate, interestRatesForInstallments, getPaymentPeriodsInOneYearCalculator(), mc); - scheduledDueDate = termVariationParams.getScheduledDueDate(); + scheduledDueDate = termVariationParams.scheduledDueDate(); if (!loanApplicationTerms.isFirstRepaymentDateAllowedOnHoliday()) { - AdjustedDateDetailsDTO adjustedDateDetailsDTO1 = this.scheduledDateGenerator.adjustRepaymentDate(scheduledDueDate, + AdjustedDateDetailsDTO adjustedDateDetailsDTO1 = getScheduledDateGenerator().adjustRepaymentDate(scheduledDueDate, loanApplicationTerms, holidayDetailDTO); scheduledDueDate = adjustedDateDetailsDTO1.getChangedScheduleDate(); } @@ -239,7 +234,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // Updates total days in term scheduleParams .addLoanTermInDays(Math.toIntExact(ChronoUnit.DAYS.between(scheduleParams.getPeriodStartDate(), scheduledDueDate))); - if (termVariationParams.isSkipPeriod()) { + if (termVariationParams.skipPeriod()) { continue; } @@ -265,7 +260,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener if (loanApplicationTerms.isInterestRecalculationEnabled()) { populateCompoundingDatesInPeriod(scheduleParams.getPeriodStartDate(), scheduledDueDate, loanApplicationTerms, - holidayDetailDTO, scheduleParams, loanCharges, currency); + holidayDetailDTO, scheduleParams, loanCharges, currency, mc); } // populates the collection with transactions till the due date of @@ -273,16 +268,15 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener Collection applicableTransactions = getApplicableTransactionsForPeriod( scheduleParams.applyInterestRecalculation(), scheduledDueDate, transactions); - final double interestCalculationGraceOnRepaymentPeriodFraction = this.paymentPeriodsInOneYearCalculator + final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction = getPaymentPeriodsInOneYearCalculator() .calculatePortionOfRepaymentPeriodInterestChargingGrace(periodStartDateApplicableForInterest, scheduledDueDate, loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(), - loanApplicationTerms.getRepaymentEvery()); + loanApplicationTerms.getRepaymentEvery(), mc); ScheduleCurrentPeriodParams currentPeriodParams = new ScheduleCurrentPeriodParams(currency, interestCalculationGraceOnRepaymentPeriodFraction); if (loanApplicationTerms.isMultiDisburseLoan()) { - updateBalanceBasedOnDisbursement(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, - scheduledDueDate); + processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, scheduledDueDate); } // process repayments to the schedule as per the repayment @@ -291,14 +285,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // for a loan repayment which falls between the // two periods for interest first repayment strategies handleRecalculationForNonDueDateTransactions(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, scheduleParams, periods, - loanApplicationTerms.getTotalInterestDue(), idealDisbursementDate, firstRepaymentdate, lastRestDate, scheduledDueDate, + loanApplicationTerms.getTotalInterestDue(), idealDisbursementDate, firstRepaymentDate, lastRestDate, scheduledDueDate, periodStartDateApplicableForInterest, applicableTransactions, currentPeriodParams); if (currentPeriodParams.isSkipCurrentLoop()) { continue; } periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, - scheduleParams.getPeriodStartDate(), idealDisbursementDate, firstRepaymentdate, + scheduleParams.getPeriodStartDate(), idealDisbursementDate, firstRepaymentDate, loanApplicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled(), loanApplicationTerms.getExpectedDisbursementDate()); @@ -307,7 +301,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // 5 determine principal,interest of repayment period PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), + getPaymentPeriodsInOneYearCalculator(), currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), scheduleParams.getTotalCumulativePrincipal().minus(scheduleParams.getReducePrincipal()), scheduleParams.getTotalCumulativeInterest(), loanApplicationTerms.getTotalInterestDue(), scheduleParams.getTotalOutstandingInterestPaymentDueToGrace(), scheduleParams.getOutstandingBalanceAsPerRest(), @@ -344,7 +338,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } // applies charges for the period - applyChargesForCurrentPeriod(loanCharges, currency, scheduleParams, scheduledDueDate, currentPeriodParams); + applyChargesForCurrentPeriod(loanCharges, currency, scheduleParams, scheduledDueDate, currentPeriodParams, mc); // sum up real totalInstallmentDue from components final Money totalInstallmentDue = currentPeriodParams.fetchTotalAmountForPeriod(); @@ -392,7 +386,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener scheduleParams.setPeriodStartDate(scheduledDueDate); scheduleParams.incrementInstalmentNumber(); scheduleParams.incrementPeriodNumber(); - if (termVariationParams.isRecalculateAmounts()) { + if (termVariationParams.recalculateAmounts()) { loanApplicationTerms.setCurrentPeriodFixedEmiAmount(null); loanApplicationTerms.setCurrentPeriodFixedPrincipalAmount(null); adjustInstallmentOrPrincipalAmount(loanApplicationTerms, scheduleParams.getTotalCumulativePrincipal(), @@ -403,7 +397,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // this condition is to add the interest from grace period if not // already applied. if (scheduleParams.getTotalOutstandingInterestPaymentDueToGrace().isGreaterThanZero()) { - LoanScheduleModelPeriod installment = ((List) periods).get(periods.size() - 1); + LoanScheduleModelPeriod installment = periods.get(periods.size() - 1); installment.addInterestAmount(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); scheduleParams.addTotalRepaymentExpected(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); scheduleParams.addTotalCumulativeInterest(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace()); @@ -412,7 +406,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // determine fees and penalties for charges which depends on total // loan interest - updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges); + updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc); // this block is to add extra re-payment schedules with interest portion // if the loan not paid with in loan term @@ -422,7 +416,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } if (scheduleParams.applyInterestRecalculation() && scheduleParams.getLatePaymentMap().size() > 0 && DateUtils.isAfter(currentDate, scheduleParams.getPeriodStartDate())) { - Money totalInterest = addInterestOnlyRepaymentScheduleForCurrentdate(mc, loanApplicationTerms, holidayDetailDTO, currency, + Money totalInterest = addInterestOnlyRepaymentScheduleForCurrentDate(mc, loanApplicationTerms, holidayDetailDTO, currency, periods, currentDate, loanRepaymentScheduleTransactionProcessor, transactions, loanCharges, scheduleParams); scheduleParams.addTotalCumulativeInterest(totalInterest); } @@ -467,21 +461,22 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } private void applyChargesForCurrentPeriod(final Set loanCharges, final MonetaryCurrency currency, - LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams) { + LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams, + final MathContext mc) { PrincipalInterest principalInterest = new PrincipalInterest(currentPeriodParams.getPrincipalForThisPeriod(), currentPeriodParams.getInterestForThisPeriod(), null); currentPeriodParams.setFeeChargesForInstallment(cumulativeFeeChargesDueWithin(scheduleParams.getPeriodStartDate(), scheduledDueDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), - scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod())); + scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc)); currentPeriodParams.setPenaltyChargesForInstallment(cumulativePenaltyChargesDueWithin(scheduleParams.getPeriodStartDate(), scheduledDueDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), - scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod())); + scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc)); scheduleParams.addTotalFeeChargesCharged(currentPeriodParams.getFeeChargesForInstallment()); scheduleParams.addTotalPenaltyChargesCharged(currentPeriodParams.getPenaltyChargesForInstallment()); } private void updatePeriodsWithCharges(final MonetaryCurrency currency, LoanScheduleParams scheduleParams, - final Collection periods, final Set nonCompoundingCharges) { + final Collection periods, final Set nonCompoundingCharges, final MathContext mc) { for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) { if (loanScheduleModelPeriod.isRepaymentPeriod()) { PrincipalInterest principalInterest = new PrincipalInterest(Money.of(currency, loanScheduleModelPeriod.principalDue()), @@ -489,11 +484,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(), - !loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod()); + !loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc); Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(), - !loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod()); + !loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc); scheduleParams.addTotalFeeChargesCharged(feeChargesForInstallment); scheduleParams.addTotalPenaltyChargesCharged(penaltyChargesForInstallment); scheduleParams.addTotalRepaymentExpected(feeChargesForInstallment.plus(penaltyChargesForInstallment)); @@ -548,17 +543,17 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), currentPeriodParams, lastTotalOutstandingInterestPaymentDueToGrace, transactionDate, modifiedInstallment, loanCharges); - Money addToPrinciapal = Money.zero(currency); + Money addToPrincipal = Money.zero(currency); if (scheduleParams.getOutstandingBalance().isLessThanZero()) { - addToPrinciapal = addToPrinciapal.plus(scheduleParams.getOutstandingBalance()); + addToPrincipal = addToPrincipal.plus(scheduleParams.getOutstandingBalance()); scheduleParams.setOutstandingBalance(Money.zero(currency)); } updateAmountsBasedOnEarlyPayment(loanApplicationTerms, holidayDetailDTO, scheduleParams, modifiedInstallment, - detail, unprocessed, addToPrinciapal); + detail, unprocessed, addToPrincipal); scheduleParams.addReducePrincipal(unprocessed); - currentPeriodParams.plusPrincipalForThisPeriod(unprocessed.plus(addToPrinciapal)); - principalProcessed = principalProcessed.plus(unprocessed.plus(addToPrinciapal)); + currentPeriodParams.plusPrincipalForThisPeriod(unprocessed.plus(addToPrincipal)); + principalProcessed = principalProcessed.plus(unprocessed.plus(addToPrincipal)); BigDecimal fixedEmiAmount = loanApplicationTerms.getFixedEmiAmount(); scheduleParams .setReducePrincipal(applyEarlyPaymentStrategy(loanApplicationTerms, scheduleParams.getReducePrincipal(), @@ -585,21 +580,21 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private LoanScheduleModelPeriod handlePrepaymentOfLoan(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, final LoanScheduleParams scheduleParams, final Money totalInterestChargedForFullLoanTerm, final LocalDate scheduledDueDate, - LocalDate periodStartDateApplicableForInterest, final double interestCalculationGraceOnRepaymentPeriodFraction, + LocalDate periodStartDateApplicableForInterest, final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final ScheduleCurrentPeriodParams currentPeriodParams, final Money lastTotalOutstandingInterestPaymentDueToGrace, final LocalDate transactionDate, final LoanScheduleModelPeriod installment, Set loanCharges) { LoanScheduleModelPeriod modifiedInstallment = installment; - Money oustanding = scheduleParams.getOutstandingBalance(); + Money outstanding = scheduleParams.getOutstandingBalance(); PrincipalInterest tempPrincipalInterest = new PrincipalInterest(currentPeriodParams.getPrincipalForThisPeriod(), currentPeriodParams.getInterestForThisPeriod(), null); - oustanding = oustanding.minus(cumulativeFeeChargesDueWithin(transactionDate, scheduledDueDate, loanCharges, + outstanding = outstanding.minus(cumulativeFeeChargesDueWithin(transactionDate, scheduledDueDate, loanCharges, totalInterestChargedForFullLoanTerm.getCurrency(), tempPrincipalInterest, scheduleParams.getPrincipalToBeScheduled(), - scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod())); - oustanding = oustanding.minus(cumulativePenaltyChargesDueWithin(transactionDate, scheduledDueDate, loanCharges, + scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc)); + outstanding = outstanding.minus(cumulativePenaltyChargesDueWithin(transactionDate, scheduledDueDate, loanCharges, totalInterestChargedForFullLoanTerm.getCurrency(), tempPrincipalInterest, scheduleParams.getPrincipalToBeScheduled(), - scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod())); + scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc)); - if (!oustanding.isGreaterThan(currentPeriodParams.getInterestForThisPeriod()) && !scheduledDueDate.equals(transactionDate)) { + if (!outstanding.isGreaterThan(currentPeriodParams.getInterestForThisPeriod()) && !scheduledDueDate.equals(transactionDate)) { final Collection interestRates = loanApplicationTerms.getLoanTermVariations().getInterestRateChanges(); LocalDate calculateTill = transactionDate; if (loanApplicationTerms.getPreClosureInterestCalculationStrategy().calculateTillRestFrequencyEnabled()) { @@ -619,7 +614,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener new TreeMap<>(scheduleParams.getCompoundingMap())); scheduleParams.getCompoundingMap().clear(); populateCompoundingDatesInPeriod(periodStartDateApplicableForInterest, calculateTill, loanApplicationTerms, holidayDetailDTO, - scheduleParams, loanCharges, totalInterestChargedForFullLoanTerm.getCurrency()); + scheduleParams, loanCharges, totalInterestChargedForFullLoanTerm.getCurrency(), mc); // this is to make sure we are recalculating using correct interest // rate @@ -631,7 +626,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } } - PrincipalInterest interestTillDate = calculatePrincipalInterestComponentsForPeriod(this.paymentPeriodsInOneYearCalculator, + PrincipalInterest interestTillDate = calculatePrincipalInterestComponentsForPeriod(getPaymentPeriodsInOneYearCalculator(), interestCalculationGraceOnRepaymentPeriodFraction, scheduleParams.getTotalCumulativePrincipal(), scheduleParams.getTotalCumulativeInterest(), totalInterestChargedForFullLoanTerm, lastTotalOutstandingInterestPaymentDueToGrace, scheduleParams.getOutstandingBalanceAsPerRest(), loanApplicationTerms, @@ -644,7 +639,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener totalInterestChargedForFullLoanTerm.getCurrency(), interestCalculationGraceOnRepaymentPeriodFraction); tempPeriod.setInterestForThisPeriod(interestTillDate.interest()); applyChargesForCurrentPeriod(loanCharges, totalInterestChargedForFullLoanTerm.getCurrency(), scheduleParams, calculateTill, - tempPeriod); + tempPeriod, mc); Money interestDiff = currentPeriodParams.getInterestForThisPeriod().minus(tempPeriod.getInterestForThisPeriod()); Money chargeDiff = currentPeriodParams.getFeeChargesForInstallment().minus(tempPeriod.getFeeChargesForInstallment()); Money penaltyDiff = currentPeriodParams.getPenaltyChargesForInstallment().minus(tempPeriod.getPenaltyChargesForInstallment()); @@ -719,7 +714,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private void handleRecalculationForNonDueDateTransactions(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final Set loanCharges, final HolidayDetailDTO holidayDetailDTO, LoanScheduleParams scheduleParams, final Collection periods, final Money totalInterestChargedForFullLoanTerm, - final LocalDate idealDisbursementDate, LocalDate firstRepaymentdate, final LocalDate lastRestDate, + final LocalDate idealDisbursementDate, LocalDate firstRepaymentDate, final LocalDate lastRestDate, final LocalDate scheduledDueDate, final LocalDate periodStartDateForInterest, final Collection applicableTransactions, final ScheduleCurrentPeriodParams currentPeriodParams) { if (scheduleParams.applyInterestRecalculation()) { @@ -755,18 +750,18 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // calculates period start date for interest // calculation as per the configuration periodStartDateApplicableForInterest = calculateInterestStartDateForPeriod(loanApplicationTerms, - scheduleParams.getPeriodStartDate(), idealDisbursementDate, firstRepaymentdate, + scheduleParams.getPeriodStartDate(), idealDisbursementDate, firstRepaymentDate, loanApplicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled(), loanApplicationTerms.getExpectedDisbursementDate()); int daysInPeriodApplicable = Math .toIntExact(ChronoUnit.DAYS.between(periodStartDateApplicableForInterest, transactionDate)); - Money interestForThisinstallment = Money.zero(currency); + Money interestForCurrentInstallment = Money.zero(currency); if (daysInPeriodApplicable > 0) { // 5 determine interest till the transaction // date PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, + getPaymentPeriodsInOneYearCalculator(), currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), scheduleParams.getTotalCumulativePrincipal().minus(scheduleParams.getReducePrincipal()), scheduleParams.getTotalCumulativeInterest(), totalInterestChargedForFullLoanTerm, @@ -775,7 +770,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener scheduleParams.getPeriodNumber(), mc, mergeVariationsToMap(scheduleParams), scheduleParams.getCompoundingMap(), periodStartDateApplicableForInterest, transactionDate, interestRates); - interestForThisinstallment = principalInterestForThisPeriod.interest(); + interestForCurrentInstallment = principalInterestForThisPeriod.interest(); scheduleParams.setTotalOutstandingInterestPaymentDueToGrace( principalInterestForThisPeriod.interestPaymentDueToGrace()); @@ -786,23 +781,23 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // applies all the applicable charges to the // newly // created installment - PrincipalInterest principalInterest = new PrincipalInterest(principalForThisPeriod, interestForThisinstallment, - null); + PrincipalInterest principalInterest = new PrincipalInterest(principalForThisPeriod, + interestForCurrentInstallment, null); Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(scheduleParams.getPeriodStartDate(), transactionDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), - scheduleParams.getTotalCumulativeInterest(), false, scheduleParams.isFirstPeriod()); + scheduleParams.getTotalCumulativeInterest(), false, scheduleParams.isFirstPeriod(), mc); Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(scheduleParams.getPeriodStartDate(), transactionDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), - scheduleParams.getTotalCumulativeInterest(), false, scheduleParams.isFirstPeriod()); + scheduleParams.getTotalCumulativeInterest(), false, scheduleParams.isFirstPeriod(), mc); // sum up real totalInstallmentDue from // components - final Money totalInstallmentDue = principalForThisPeriod.plus(interestForThisinstallment) + final Money totalInstallmentDue = principalForThisPeriod.plus(interestForCurrentInstallment) .plus(feeChargesForInstallment).plus(penaltyChargesForInstallment); // create repayment period from parts installment = LoanScheduleModelRepaymentPeriod.repayment(scheduleParams.getInstalmentNumber(), scheduleParams.getPeriodStartDate(), transactionDate, principalForThisPeriod, - scheduleParams.getOutstandingBalance(), interestForThisinstallment, feeChargesForInstallment, + scheduleParams.getOutstandingBalance(), interestForCurrentInstallment, feeChargesForInstallment, penaltyChargesForInstallment, totalInstallmentDue, true); periods.add(installment); addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment); @@ -815,7 +810,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // handle cumulative fields scheduleParams.addLoanTermInDays(periodDays); scheduleParams.addTotalRepaymentExpected(totalInstallmentDue); - scheduleParams.addTotalCumulativeInterest(interestForThisinstallment); + scheduleParams.addTotalCumulativeInterest(interestForCurrentInstallment); scheduleParams.addTotalFeeChargesCharged(feeChargesForInstallment); scheduleParams.addTotalPenaltyChargesCharged(penaltyChargesForInstallment); @@ -824,7 +819,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener updateLatePaymentMap = true; scheduleParams.incrementInstalmentNumber(); populateCompoundingDatesInPeriod(scheduleParams.getPeriodStartDate(), scheduledDueDate, loanApplicationTerms, - holidayDetailDTO, scheduleParams, loanCharges, currency); + holidayDetailDTO, scheduleParams, loanCharges, currency, mc); // creates and insert Loan repayment schedule // for // the period @@ -850,9 +845,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener scheduleParams.reduceOutstandingBalance(unprocessed); // if outstanding balance becomes less than zero // then adjusts the princiapal - Money addToPrinciapal = Money.zero(currency); + Money addToPrincipal = Money.zero(currency); if (!scheduleParams.getOutstandingBalance().isGreaterThanZero()) { - addToPrinciapal = addToPrinciapal.plus(scheduleParams.getOutstandingBalance()); + addToPrincipal = addToPrincipal.plus(scheduleParams.getOutstandingBalance()); scheduleParams.setOutstandingBalance(Money.zero(currency)); currentPeriodParams.setLastInstallment(installment); } @@ -860,7 +855,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // payment amounts and applicable date as per // rest updateAmountsBasedOnEarlyPayment(loanApplicationTerms, holidayDetailDTO, scheduleParams, installment, detail, - unprocessed, addToPrinciapal); + unprocessed, addToPrincipal); // method applies early payment strategy scheduleParams.addReducePrincipal(unprocessed); @@ -896,16 +891,15 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener scheduleParams.getCompoundingDateVariations().put(periodStartDateApplicableForInterest, new TreeMap<>(scheduleParams.getCompoundingMap())); - LocalDate calculateTill = transactionDate; PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, + getPaymentPeriodsInOneYearCalculator(), currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), scheduleParams.getTotalCumulativePrincipal().minus(scheduleParams.getReducePrincipal()), scheduleParams.getTotalCumulativeInterest(), totalInterestChargedForFullLoanTerm, scheduleParams.getTotalOutstandingInterestPaymentDueToGrace(), scheduleParams.getOutstandingBalanceAsPerRest(), loanApplicationTerms, scheduleParams.getPeriodNumber(), mc, mergeVariationsToMap(scheduleParams), - scheduleParams.getCompoundingMap(), periodStartDateApplicableForInterest, calculateTill, + scheduleParams.getCompoundingMap(), periodStartDateApplicableForInterest, transactionDate, interestRates); if (!principalInterestForThisPeriod.interest() .plus(principalInterestForThisPeriod.interestPaymentDueToGrace()) @@ -929,12 +923,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // if outstanding balance becomes less than // zero // then adjusts the princiapal - Money addToPrinciapal = Money.zero(currency); + Money addToPrincipal = Money.zero(currency); if (scheduleParams.getOutstandingBalance().isLessThanZero()) { - addToPrinciapal = addToPrinciapal.plus(scheduleParams.getOutstandingBalance()); + addToPrincipal = addToPrincipal.plus(scheduleParams.getOutstandingBalance()); scheduleParams.setOutstandingBalance(Money.zero(currency)); - updateMapWithAmount(scheduleParams.getPrincipalPortionMap(), addToPrinciapal, applicableDate); - currentPeriodParams.plusEarlyPaidAmount(addToPrinciapal); + updateMapWithAmount(scheduleParams.getPrincipalPortionMap(), addToPrincipal, applicableDate); + currentPeriodParams.plusEarlyPaidAmount(addToPrincipal); } } @@ -970,11 +964,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private void updateAmountsBasedOnEarlyPayment(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, LoanScheduleParams scheduleParams, final LoanScheduleModelPeriod installment, RecalculationDetail detail, Money unprocessed, - Money addToPrinciapal) { + Money addToPrincipal) { updatePrincipalPaidPortionToMap(loanApplicationTerms, holidayDetailDTO, scheduleParams.getPrincipalPortionMap(), installment, - detail, unprocessed.plus(addToPrinciapal), scheduleParams.getInstallments()); - scheduleParams.addTotalRepaymentExpected(unprocessed.plus(addToPrinciapal)); - scheduleParams.addTotalCumulativePrincipal(unprocessed.plus(addToPrinciapal)); + detail, unprocessed.plus(addToPrincipal), scheduleParams.getInstallments()); + scheduleParams.addTotalRepaymentExpected(unprocessed.plus(addToPrincipal)); + scheduleParams.addTotalCumulativePrincipal(unprocessed.plus(addToPrincipal)); } private void updateOutstandingBalanceAsPerRest(final LoanScheduleParams scheduleParams, final LocalDate scheduledDueDate) { @@ -985,12 +979,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } /** - * Method updates outstanding balance of the loan for interest calculation + * Method add extra disbursement periods (if applicable) and update the schedule params * */ - private void updateBalanceBasedOnDisbursement(final LoanApplicationTerms loanApplicationTerms, - final BigDecimal chargesDueAtTimeOfDisbursement, LoanScheduleParams scheduleParams, - final Collection periods, final LocalDate scheduledDueDate) { + private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement, + LoanScheduleParams scheduleParams, final Collection periods, final LocalDate scheduledDueDate) { for (Map.Entry disburseDetail : scheduleParams.getDisburseDetailMap().entrySet()) { if (DateUtils.isAfter(disburseDetail.getKey(), scheduleParams.getPeriodStartDate()) && !DateUtils.isAfter(disburseDetail.getKey(), scheduledDueDate)) { @@ -1064,9 +1057,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener loanApplicationTerms.setFixedEmiAmount(null); } } else { - Money totalInterestDueForLoan = Money.zero(loanApplicationTerms.getCurrency()); loanApplicationTerms.setTotalPrincipalAccountedForInterestCalculation(scheduleParams.getTotalCumulativePrincipal()); - totalInterestDueForLoan = loanApplicationTerms.calculateTotalInterestCharged(calculator, mc); + Money totalInterestDueForLoan = loanApplicationTerms.calculateTotalInterestCharged(calculator, mc); totalInterestDueForLoan = totalInterestDueForLoan.plus(scheduleParams.getTotalCumulativeInterest()); loanApplicationTerms.updateTotalInterestDue(totalInterestDueForLoan); // exclude till last period in calculations @@ -1126,7 +1118,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener Integer rescheduleNumberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); rescheduleNumberOfRepayments += loanTermVariationsData.getDecimalValue().intValue(); loanApplicationTerms.updateNumberOfRepayments(rescheduleNumberOfRepayments); - LocalDate loanEndDate = this.scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, + LocalDate loanEndDate = getScheduledDateGenerator().getLastRepaymentDate(loanApplicationTerms, loanApplicationTerms.getHolidayDetailDTO()); loanApplicationTerms.updateLoanEndDate(loanEndDate); loanApplicationTerms.updateAccountedTillPeriod(scheduleParams.getPeriodNumber() - 1, @@ -1156,9 +1148,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } } - LoanTermVariationParams termVariationParams = new LoanTermVariationParams(skipPeriod, recalculateAmounts, modifiedScheduledDueDate, - variationsData); - return termVariationParams; + return new LoanTermVariationParams(skipPeriod, recalculateAmounts, modifiedScheduledDueDate, variationsData); } /** @@ -1231,7 +1221,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener rescheduleNumberOfRepayments += loanTermVariationsData.getDecimalValue().intValue(); loanApplicationTerms.updateNumberOfRepayments(rescheduleNumberOfRepayments); // generate list of proposed schedule due dates - LocalDate loanEndDate = this.scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, + LocalDate loanEndDate = getScheduledDateGenerator().getLastRepaymentDate(loanApplicationTerms, loanApplicationTerms.getHolidayDetailDTO()); loanApplicationTerms.updateLoanEndDate(loanEndDate); adjustInstallmentOrPrincipalAmount(loanApplicationTerms, totalCumulativePrincipal, instalmentNumber, mc); @@ -1244,9 +1234,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } } - LoanTermVariationParams termVariationParams = new LoanTermVariationParams(skipPeriod, recalculateAmounts, modifiedScheduledDueDate, - variationsData); - return termVariationParams; + return new LoanTermVariationParams(skipPeriod, recalculateAmounts, modifiedScheduledDueDate, variationsData); } /** @@ -1322,7 +1310,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener if (periodNumber < loanApplicationTerms.getPrincipalGrace() + 1) { periodNumber = loanApplicationTerms.getPrincipalGrace() + 1; } - Money emiAmount = loanApplicationTerms.pmtForInstallment(this.paymentPeriodsInOneYearCalculator, outstandingBalance, + Money emiAmount = loanApplicationTerms.pmtForInstallment(getPaymentPeriodsInOneYearCalculator(), outstandingBalance, periodNumber, mc); loanApplicationTerms.setFixedEmiAmount(emiAmount.getAmount()); isAmountChanged = true; @@ -1355,7 +1343,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener * current date and adds new repayment schedule detail * */ - private Money addInterestOnlyRepaymentScheduleForCurrentdate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, + private Money addInterestOnlyRepaymentScheduleForCurrentDate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, final MonetaryCurrency currency, final Collection periods, final LocalDate currentDate, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final Collection transactions, final Set loanCharges, final LoanScheduleParams params) { @@ -1364,7 +1352,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener Money outstanding = params.getOutstandingBalanceAsPerRest(); Money totalInterest = Money.zero(currency); Money totalCumulativeInterest = Money.zero(currency); - double interestCalculationGraceOnRepaymentPeriodFraction = 0; + BigDecimal interestCalculationGraceOnRepaymentPeriodFraction = BigDecimal.ZERO; int periodNumberTemp = 1; LocalDate lastRestDate = getNextRestScheduleDate(currentDate.minusDays(1), loanApplicationTerms, holidayDetailDTO); Collection applicableVariations = loanApplicationTerms.getLoanTermVariations().getInterestRateChanges(); @@ -1372,7 +1360,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener LocalDate additionalPeriodsStartDate = params.getPeriodStartDate(); do { - params.setActualRepaymentDate(this.scheduledDateGenerator.generateNextRepaymentDate(params.getActualRepaymentDate(), + params.setActualRepaymentDate(getScheduledDateGenerator().generateNextRepaymentDate(params.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment)); if (DateUtils.isAfter(params.getActualRepaymentDate(), currentDate)) { params.setActualRepaymentDate(currentDate); @@ -1382,7 +1370,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener params.getActualRepaymentDate(), transactions); populateCompoundingDatesInPeriod(params.getPeriodStartDate(), params.getActualRepaymentDate(), loanApplicationTerms, - holidayDetailDTO, params, loanCharges, currency); + holidayDetailDTO, params, loanCharges, currency, mc); for (RecalculationDetail detail : applicableTransactions) { if (detail.isProcessed()) { @@ -1393,7 +1381,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener if (!DateUtils.isEqual(transactionDate, params.getPeriodStartDate())) { PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalInterest.zero(), + getPaymentPeriodsInOneYearCalculator(), interestCalculationGraceOnRepaymentPeriodFraction, totalInterest.zero(), totalInterest.zero(), totalInterest.zero(), totalInterest.zero(), outstanding, loanApplicationTerms, periodNumberTemp, mc, mergeVariationsToMap(params), params.getCompoundingMap(), params.getPeriodStartDate(), transactionDate, applicableVariations); @@ -1411,7 +1399,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener addLoanRepaymentScheduleInstallment(params.getInstallments(), installment); updateCompoundingMap(loanApplicationTerms, holidayDetailDTO, params, lastRestDate, transactionDate); populateCompoundingDatesInPeriod(installment.periodDueDate(), params.getActualRepaymentDate(), loanApplicationTerms, - holidayDetailDTO, params, loanCharges, currency); + holidayDetailDTO, params, loanCharges, currency, mc); uncompoundedFromLastInstallment = params.getUnCompoundedAmount(); params.setPeriodStartDate(transactionDate); startDate = transactionDate; @@ -1436,7 +1424,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener if (!outstanding.isZero()) { PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( - this.paymentPeriodsInOneYearCalculator, interestCalculationGraceOnRepaymentPeriodFraction, totalInterest.zero(), + getPaymentPeriodsInOneYearCalculator(), interestCalculationGraceOnRepaymentPeriodFraction, totalInterest.zero(), totalInterest.zero(), totalInterest.zero(), totalInterest.zero(), outstanding, loanApplicationTerms, periodNumberTemp, mc, mergeVariationsToMap(params), params.getCompoundingMap(), params.getPeriodStartDate(), params.getActualRepaymentDate(), applicableVariations); @@ -1569,7 +1557,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener */ private Money updateBalanceForInterestCalculation(final Map principalPortionMap, final LocalDate scheduledDueDate, final Money outstandingBalanceAsPerRest, boolean addMapDetails) { - List removeFromprincipalPortionMap = new ArrayList<>(); + List removeFromPrincipalPortionMap = new ArrayList<>(); Money outstandingBalance = outstandingBalanceAsPerRest; for (Map.Entry principal : principalPortionMap.entrySet()) { if (!DateUtils.isAfter(principal.getKey(), scheduledDueDate)) { @@ -1578,10 +1566,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } else { outstandingBalance = outstandingBalance.minus(principal.getValue()); } - removeFromprincipalPortionMap.add(principal.getKey()); + removeFromPrincipalPortionMap.add(principal.getKey()); } } - for (LocalDate date : removeFromprincipalPortionMap) { + for (LocalDate date : removeFromPrincipalPortionMap) { principalPortionMap.remove(date); } return outstandingBalance; @@ -1682,10 +1670,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener final boolean isPastDate = params.applyInterestRecalculation() && DateUtils.isBeforeBusinessDate(loanRepaymentScheduleInstallment.getDueDate()); boolean periodHasCompoundingDate = false; - Money amountCharged = Money.zero(currency); - if (loanApplicationTerms.getInterestRecalculationCompoundingMethod() != null) { - amountCharged = getIncomeForCompounding(loanApplicationTerms, currency, loanRepaymentScheduleInstallment); - } + Money amountCharged = getIncomeForCompounding(loanApplicationTerms, currency, loanRepaymentScheduleInstallment); final Map compoundingMap = params.getCompoundingMap(); LocalDate effectiveStartDate = loanRepaymentScheduleInstallment.getFromDate(); if (loanApplicationTerms.allowCompoundingOnEod()) { @@ -1709,8 +1694,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener updateMapWithAmount(compoundingMap, amountToBeCompounding, compoundingEffectiveDate); totalCompoundedAmount = totalCompoundedAmount.plus(amountToBeCompounding); } else if (compoundingMap.containsKey(compoundingEffectiveDate)) { - Money compounedAmount = compoundingMap.get(compoundingEffectiveDate); - totalCompoundedAmount = totalCompoundedAmount.plus(compounedAmount); + Money compoundedAmount = compoundingMap.get(compoundingEffectiveDate); + totalCompoundedAmount = totalCompoundedAmount.plus(compoundedAmount); } if (!loanApplicationTerms.allowCompoundingOnEod()) { @@ -1762,7 +1747,6 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener Money interestCharged = Money.zero(currency); Money feeCharged = Money.zero(currency); Money penaltyCharged = Money.zero(currency); - Money amountCharged = Money.zero(currency); switch (loanApplicationTerms.getInterestRecalculationCompoundingMethod()) { case INTEREST: interestCharged = interestCharged.plus(loanRepaymentScheduleInstallment.getInterestCharged(currency)); @@ -1779,8 +1763,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener default: break; } - amountCharged = interestCharged.plus(feeCharged).plus(penaltyCharged); - return amountCharged; + return interestCharged.plus(feeCharged).plus(penaltyCharged); } private void adjustCompoundedAmountWithPaidDetail(final LoanScheduleParams params, final LocalDate lastRestDate, @@ -1811,7 +1794,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private void populateCompoundingDatesInPeriod(final LocalDate startDate, final LocalDate endDate, final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, - final LoanScheduleParams scheduleParams, final Set charges, MonetaryCurrency currency) { + final LoanScheduleParams scheduleParams, final Set charges, MonetaryCurrency currency, final MathContext mc) { if (loanApplicationTerms.getInterestRecalculationCompoundingMethod().isCompoundingEnabled()) { final Map compoundingMap = scheduleParams.getCompoundingMap(); LocalDate lastCompoundingDate = startDate; @@ -1826,9 +1809,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener if (DateUtils.isBefore(compoundingDate, endDate)) { boolean isFirst = DateUtils.isEqual(startDate, lastCompoundingDate); Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(lastCompoundingDate, compoundingDate, charges, currency, - null, loanApplicationTerms.getPrincipal(), null, false, isFirst); + null, loanApplicationTerms.getPrincipal(), null, false, isFirst, mc); Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(lastCompoundingDate, compoundingDate, charges, - currency, null, loanApplicationTerms.getPrincipal(), null, false, isFirst); + currency, null, loanApplicationTerms.getPrincipal(), null, false, isFirst, mc); Money compoundAmount = feeChargesForInstallment.plus(penaltyChargesForInstallment); if (addUncompounded) { compoundAmount = compoundAmount.plus(scheduleParams.getUnCompoundedAmount()); @@ -1864,8 +1847,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener * */ private TreeMap mergeVariationsToMap(final LoanScheduleParams params) { - TreeMap map = new TreeMap<>(); - map.putAll(params.getLatePaymentMap()); + TreeMap map = new TreeMap<>(params.getLatePaymentMap()); for (Map.Entry mapEntry : params.getDisburseDetailMap().entrySet()) { Money value = mapEntry.getValue(); if (map.containsKey(mapEntry.getKey())) { @@ -1895,14 +1877,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener /** * calculates Interest stating date as per the settings * - * @param firstRepaymentdate + * @param firstRepaymentDate * TODO */ private LocalDate calculateInterestStartDateForPeriod(final LoanApplicationTerms loanApplicationTerms, LocalDate periodStartDate, - final LocalDate idealDisbursementDate, final LocalDate firstRepaymentdate, + final LocalDate idealDisbursementDate, final LocalDate firstRepaymentDate, final Boolean isInterestChargedFromDateSameAsDisbursalDateEnabled, final LocalDate expectedDisbursementDate) { LocalDate periodStartDateApplicableForInterest = periodStartDate; - if (DateUtils.isBefore(periodStartDate, idealDisbursementDate) || DateUtils.isBefore(periodStartDate, firstRepaymentdate)) { + if (DateUtils.isBefore(periodStartDate, idealDisbursementDate) || DateUtils.isBefore(periodStartDate, firstRepaymentDate)) { if (loanApplicationTerms.getInterestChargedFromLocalDate() != null) { if (DateUtils.isEqual(periodStartDate, loanApplicationTerms.getExpectedDisbursementDate()) || DateUtils.isBefore(periodStartDate, loanApplicationTerms.getInterestChargedFromLocalDate())) { @@ -1926,17 +1908,18 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } + // Abstract methods + public abstract ScheduledDateGenerator getScheduledDateGenerator(); + + public abstract PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator(); + public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator, - double interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest, + BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest, Money totalInterestDueForLoan, Money cumulatingInterestPaymentDueToGrace, Money outstandingBalance, LoanApplicationTerms loanApplicationTerms, int periodNumber, MathContext mc, TreeMap principalVariation, Map compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate, Collection termVariations); - protected final boolean isLastRepaymentPeriod(final int numberOfRepayments, final int periodNumber) { - return periodNumber == numberOfRepayments; - } - private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set loanCharges) { BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; for (final LoanCharge loanCharge : loanCharges) { @@ -1948,7 +1931,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } private BigDecimal getDisbursementAmount(final LoanApplicationTerms loanApplicationTerms, LocalDate disbursementDate, - final Map disburseDetails, final boolean excludePastUndisbursed) { + final Map disburseDetails, final boolean excludePastUnDisbursed) { // this method relates to multi-disbursement loans BigDecimal principal = BigDecimal.ZERO; @@ -1965,16 +1948,16 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) { if (disbursementData.disbursementDate().equals(disbursementDate)) { principal = principal.add(disbursementData.getPrincipal()); - } else if (!excludePastUndisbursed || disbursementData.isDisbursed() + } else if (!excludePastUnDisbursed || disbursementData.isDisbursed() || !DateUtils.isBeforeBusinessDate(disbursementData.disbursementDate())) { /* * JW: sums up amounts by disbursal date in case of side-effect issues. Original assumed that there * were no duplicate disbursal dates and 'put' each amount into the map keyed by date */ - Money prevsum = disburseDetails.get(disbursementData.disbursementDate()); + Money previousSum = disburseDetails.get(disbursementData.disbursementDate()); BigDecimal sumToNow = BigDecimal.ZERO; - if (prevsum != null) { - sumToNow = prevsum.getAmount(); + if (previousSum != null) { + sumToNow = previousSum.getAmount(); } sumToNow = sumToNow.add(disbursementData.getPrincipal()); disburseDetails.put(disbursementData.disbursementDate(), Money.of(currency, sumToNow)); @@ -1984,10 +1967,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return principal; } - private Collection createNewLoanScheduleListWithDisbursementDetails( - final LoanApplicationTerms loanApplicationTerms, final LoanScheduleParams loanScheduleParams, - final BigDecimal chargesDueAtTimeOfDisbursement) { - Collection periods = new ArrayList<>(); + private List createNewLoanScheduleListWithDisbursementDetails(final LoanApplicationTerms loanApplicationTerms, + final LoanScheduleParams loanScheduleParams, final BigDecimal chargesDueAtTimeOfDisbursement) { + List periods = new ArrayList<>(); if (!loanApplicationTerms.isMultiDisburseLoan()) { final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod .disbursement(loanApplicationTerms, chargesDueAtTimeOfDisbursement); @@ -2042,7 +2024,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return installment; } - private Set seperateTotalCompoundingPercentageCharges(final Set loanCharges) { + private Set separateTotalCompoundingPercentageCharges(final Set loanCharges) { Set interestCharges = new HashSet<>(); for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest() @@ -2056,7 +2038,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, - final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod) { + final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, + final MathContext mc) { Money cumulative = Money.zero(monetaryCurrency); @@ -2065,12 +2048,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { - cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge); + cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge, mc); } else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) { cumulative = cumulative.plus(loanCharge.chargeAmount()); } else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) { cumulative = calculateSpecificDueDateChargeWithPercentage(principalDisbursed, totalInterestChargedForFullLoanTerm, - cumulative, loanCharge); + cumulative, loanCharge, mc); } else if (isDue) { cumulative = cumulative.plus(loanCharge.amount()); } @@ -2081,7 +2064,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } private Money calculateSpecificDueDateChargeWithPercentage(final Money principalDisbursed, - final Money totalInterestChargedForFullLoanTerm, Money cumulative, final LoanCharge loanCharge) { + final Money totalInterestChargedForFullLoanTerm, Money cumulative, final LoanCharge loanCharge, final MathContext mc) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { amount = amount.add(principalDisbursed.getAmount()).add(totalInterestChargedForFullLoanTerm.getAmount()); @@ -2090,13 +2073,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } else { amount = amount.add(principalDisbursed.getAmount()); } - BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); + BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc); cumulative = cumulative.plus(loanChargeAmt); return cumulative; } private Money calculateInstallmentCharge(final PrincipalInterest principalInterestForThisPeriod, Money cumulative, - final LoanCharge loanCharge) { + final LoanCharge loanCharge, final MathContext mc) { if (loanCharge.getChargeCalculation().isPercentageBased()) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { @@ -2107,7 +2090,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } else { amount = amount.add(principalInterestForThisPeriod.principal().getAmount()); } - BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100)); + BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc); cumulative = cumulative.plus(loanChargeAmt); } else { cumulative = cumulative.plus(loanCharge.amountOrPercentage()); @@ -2118,7 +2101,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, - final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod) { + final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, + final MathContext mc) { Money cumulative = Money.zero(monetaryCurrency); @@ -2127,12 +2111,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { - cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge); + cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge, mc); } else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) { cumulative = cumulative.plus(loanCharge.chargeAmount()); } else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) { cumulative = calculateSpecificDueDateChargeWithPercentage(principalDisbursed, totalInterestChargedForFullLoanTerm, - cumulative, loanCharge); + cumulative, loanCharge, mc); } else if (isDue) { cumulative = cumulative.plus(loanCharge.amount()); } @@ -2176,7 +2160,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener LoanScheduleParams loanScheduleParams = LoanScheduleParams.createLoanScheduleParamsForCompleteUpdate(recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate, applyInterestRecalculation); - Collection periods = new ArrayList<>(); + List periods = new ArrayList<>(); final List retainedInstallments = new ArrayList<>(); // this block is to retain the schedule installments prior to the @@ -2302,14 +2286,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // check for date changes do { - actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, + actualRepaymentDate = getScheduledDateGenerator().generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms, isFirstRepayment); if (!DateUtils.isBefore(actualRepaymentDate, rescheduleFrom)) { actualRepaymentDate = lastInstallmentDate; } isFirstRepayment = false; LocalDate prevLastInstDate = lastInstallmentDate; - lastInstallmentDate = this.scheduledDateGenerator + lastInstallmentDate = getScheduledDateGenerator() .adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms, holidayDetailDTO).getChangedScheduleDate(); LocalDate modifiedLastInstDate = null; LoanTermVariationsData variation1 = null; @@ -2338,7 +2322,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener loanTermVariationParams = applyExceptionLoanTermVariations(loanApplicationTerms, lastInstallmentDate, exceptionDataListIterator, instalmentNumber, totalCumulativePrincipal, totalCumulativeInterest, mc); - } while (loanTermVariationParams != null && loanTermVariationParams.isSkipPeriod()); + } while (loanTermVariationParams != null && loanTermVariationParams.skipPeriod()); periodNumber++; @@ -2346,8 +2330,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener dueDateVariation.setProcessed(true); } - if (loanTermVariationParams != null && loanTermVariationParams.isSkipPeriod()) { - ArrayList variationsDataList = loanTermVariationParams.getVariationsDataList(); + if (loanTermVariationParams != null && loanTermVariationParams.skipPeriod()) { + List variationsDataList = loanTermVariationParams.variationsData(); for (LoanTermVariationsData variationsData : variationsDataList) { variationsData.setProcessed(true); } @@ -2501,21 +2485,21 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } } periods.addAll(loanScheduleModel.getPeriods()); - LoanScheduleModel loanScheduleModelwithPeriodChanges = LoanScheduleModel.withLoanScheduleModelPeriods(periods, loanScheduleModel); - return LoanScheduleDTO.from(retainedInstallments, loanScheduleModelwithPeriodChanges); + LoanScheduleModel loanScheduleModelWithPeriodChanges = LoanScheduleModel.withLoanScheduleModelPeriods(periods, loanScheduleModel); + return LoanScheduleDTO.from(retainedInstallments, loanScheduleModelWithPeriodChanges); } private List fetchRetainedInstallments( final List repaymentScheduleInstallments, final LocalDate rescheduleFrom, MonetaryCurrency currency) { List newRepaymentScheduleInstallments = new ArrayList<>(); - int lastInterestAvilablePeriod = 0; + int lastInterestAvailablePeriod = 0; int processedPeriod = 0; for (LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) { if (DateUtils.isBefore(installment.getDueDate(), rescheduleFrom)) { newRepaymentScheduleInstallments.add(installment); if (installment.getInterestCharged(currency).isGreaterThanZero()) { - lastInterestAvilablePeriod = installment.getInstallmentNumber(); + lastInterestAvailablePeriod = installment.getInstallmentNumber(); } processedPeriod = installment.getInstallmentNumber(); } else { @@ -2528,10 +2512,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // if the last retained period is interest grace period then we // can't get the interest of last period without calculating again // to fix this adjusting retained periods - if (lastInterestAvilablePeriod != processedPeriod) { + if (lastInterestAvailablePeriod != processedPeriod) { final List retainRepaymentScheduleInstallments = new ArrayList<>(); for (LoanRepaymentScheduleInstallment installment : newRepaymentScheduleInstallments) { - if (installment.getInstallmentNumber() <= lastInterestAvilablePeriod) { + if (installment.getInstallmentNumber() <= lastInterestAvailablePeriod) { retainRepaymentScheduleInstallments.add(installment); } } @@ -2706,19 +2690,18 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private LoanScheduleModelPeriod createLoanScheduleModelPeriod(final LoanRepaymentScheduleInstallment installment, final Money outstandingPrincipal) { final MonetaryCurrency currency = outstandingPrincipal.getCurrency(); - LoanScheduleModelPeriod scheduledLoanInstallment = LoanScheduleModelRepaymentPeriod.repayment(installment.getInstallmentNumber(), - installment.getFromDate(), installment.getDueDate(), installment.getPrincipal(currency), outstandingPrincipal, + return LoanScheduleModelRepaymentPeriod.repayment(installment.getInstallmentNumber(), installment.getFromDate(), + installment.getDueDate(), installment.getPrincipal(currency), outstandingPrincipal, installment.getInterestCharged(currency), installment.getFeeChargesCharged(currency), installment.getPenaltyChargesCharged(currency), installment.getDue(currency), installment.isRecalculatedInterestComponent()); - return scheduledLoanInstallment; } private LocalDate getNextRestScheduleDate(LocalDate startDate, LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) { - LocalDate nextScheduleDate = null; + LocalDate nextScheduleDate; if (loanApplicationTerms.getRecalculationFrequencyType().isSameAsRepayment()) { - nextScheduleDate = this.scheduledDateGenerator.generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(startDate, + nextScheduleDate = getScheduledDateGenerator().generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(startDate, loanApplicationTerms, holidayDetailDTO); } else { CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance(); @@ -2730,12 +2713,12 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener private LocalDate getNextCompoundScheduleDate(LocalDate startDate, LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) { - LocalDate nextScheduleDate = null; + LocalDate nextScheduleDate; if (!loanApplicationTerms.getInterestRecalculationCompoundingMethod().isCompoundingEnabled()) { return null; } if (loanApplicationTerms.getCompoundingFrequencyType().isSameAsRepayment()) { - nextScheduleDate = this.scheduledDateGenerator.generateNextScheduleDateStartingFromDisburseDate(startDate, loanApplicationTerms, + nextScheduleDate = getScheduledDateGenerator().generateNextScheduleDateStartingFromDisburseDate(startDate, loanApplicationTerms, holidayDetailDTO); } else { CalendarInstance calendarInstance = loanApplicationTerms.getCompoundingCalendarInstance(); @@ -2783,172 +2766,4 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener return new LoanRepaymentScheduleInstallment(null, 0, onDate, onDate, totalPrincipal.getAmount(), totalInterest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount(), false, compoundingDetails); } - - private static final class LoanTermVariationParams { - - private final boolean skipPeriod; - private final boolean recalculateAmounts; - private final LocalDate scheduledDueDate; - private final ArrayList variationsData; - - LoanTermVariationParams(final boolean skipPeriod, final boolean recalculateAmounts, final LocalDate scheduledDueDate, - final ArrayList variationsData) { - this.skipPeriod = skipPeriod; - this.recalculateAmounts = recalculateAmounts; - this.scheduledDueDate = scheduledDueDate; - this.variationsData = variationsData; - } - - public boolean isSkipPeriod() { - return this.skipPeriod; - } - - public boolean isRecalculateAmounts() { - return this.recalculateAmounts; - } - - public LocalDate getScheduledDueDate() { - return this.scheduledDueDate; - } - - public ArrayList getVariationsDataList() { - return this.variationsData; - } - - } - - private static final class ScheduleCurrentPeriodParams { - - Money earlyPaidAmount; - LoanScheduleModelPeriod lastInstallment; - boolean skipCurrentLoop; - Money interestForThisPeriod; - Money principalForThisPeriod; - Money feeChargesForInstallment; - Money penaltyChargesForInstallment; - // for adjusting outstandingBalances - Money reducedBalance; - boolean isEmiAmountChanged; - double interestCalculationGraceOnRepaymentPeriodFraction; - - ScheduleCurrentPeriodParams(final MonetaryCurrency currency, double interestCalculationGraceOnRepaymentPeriodFraction) { - this.earlyPaidAmount = Money.zero(currency); - this.lastInstallment = null; - this.skipCurrentLoop = false; - this.interestForThisPeriod = Money.zero(currency); - this.principalForThisPeriod = Money.zero(currency); - this.reducedBalance = Money.zero(currency); - this.feeChargesForInstallment = Money.zero(currency); - this.penaltyChargesForInstallment = Money.zero(currency); - this.isEmiAmountChanged = false; - this.interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction; - } - - public Money getEarlyPaidAmount() { - return this.earlyPaidAmount; - } - - public void plusEarlyPaidAmount(Money earlyPaidAmount) { - this.earlyPaidAmount = this.earlyPaidAmount.plus(earlyPaidAmount); - } - - public void minusEarlyPaidAmount(Money earlyPaidAmount) { - this.earlyPaidAmount = this.earlyPaidAmount.minus(earlyPaidAmount); - } - - public LoanScheduleModelPeriod getLastInstallment() { - return this.lastInstallment; - } - - public void setLastInstallment(LoanScheduleModelPeriod lastInstallment) { - this.lastInstallment = lastInstallment; - } - - public boolean isSkipCurrentLoop() { - return this.skipCurrentLoop; - } - - public void setSkipCurrentLoop(boolean skipCurrentLoop) { - this.skipCurrentLoop = skipCurrentLoop; - } - - public Money getInterestForThisPeriod() { - return this.interestForThisPeriod; - } - - public void setInterestForThisPeriod(Money interestForThisPeriod) { - this.interestForThisPeriod = interestForThisPeriod; - } - - public void minusInterestForThisPeriod(Money interestForThisPeriod) { - this.interestForThisPeriod = this.interestForThisPeriod.minus(interestForThisPeriod); - } - - public Money getPrincipalForThisPeriod() { - return this.principalForThisPeriod; - } - - public void setPrincipalForThisPeriod(Money principalForThisPeriod) { - this.principalForThisPeriod = principalForThisPeriod; - } - - public void plusPrincipalForThisPeriod(Money principalForThisPeriod) { - this.principalForThisPeriod = this.principalForThisPeriod.plus(principalForThisPeriod); - } - - public void minusPrincipalForThisPeriod(Money principalForThisPeriod) { - this.principalForThisPeriod = this.principalForThisPeriod.minus(principalForThisPeriod); - } - - public Money getReducedBalance() { - return this.reducedBalance; - } - - public void setReducedBalance(Money reducedBalance) { - this.reducedBalance = reducedBalance; - } - - public Money getFeeChargesForInstallment() { - return this.feeChargesForInstallment; - } - - public void setFeeChargesForInstallment(Money feeChargesForInstallment) { - this.feeChargesForInstallment = feeChargesForInstallment; - } - - public void minusFeeChargesForInstallment(Money feeChargesForInstallment) { - this.feeChargesForInstallment = this.feeChargesForInstallment.minus(feeChargesForInstallment); - } - - public Money getPenaltyChargesForInstallment() { - return this.penaltyChargesForInstallment; - } - - public void setPenaltyChargesForInstallment(Money penaltyChargesForInstallment) { - this.penaltyChargesForInstallment = penaltyChargesForInstallment; - } - - public void minusPenaltyChargesForInstallment(Money penaltyChargesForInstallment) { - this.penaltyChargesForInstallment = this.penaltyChargesForInstallment.minus(penaltyChargesForInstallment); - } - - public Money fetchTotalAmountForPeriod() { - return this.principalForThisPeriod.plus(interestForThisPeriod).plus(feeChargesForInstallment) - .plus(penaltyChargesForInstallment); - } - - public boolean isEmiAmountChanged() { - return this.isEmiAmountChanged; - } - - public void setEmiAmountChanged(boolean isEmiAmountChanged) { - this.isEmiAmountChanged = isEmiAmountChanged; - } - - public double getInterestCalculationGraceOnRepaymentPeriodFraction() { - return this.interestCalculationGraceOnRepaymentPeriodFraction; - } - - } - } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java new file mode 100644 index 000000000..8912fad65 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java @@ -0,0 +1,629 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO; +import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; +import org.apache.fineract.portfolio.calendar.service.CalendarUtils; +import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; +import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; +import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; +import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; + +public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { + + @Override + public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, + final Set loanCharges, final HolidayDetailDTO holidayDetailDTO) { + // TODO: handle interest calculation + final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency(); + // generate list of proposed schedule due dates + LocalDate loanEndDate = getScheduledDateGenerator().getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); + LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations() + .fetchLoanTermDueDateVariationsData(loanEndDate); + if (lastDueDateVariation != null) { + loanEndDate = lastDueDateVariation.getDateValue(); + } + loanApplicationTerms.updateLoanEndDate(loanEndDate); + + // determine the total charges due at time of disbursement + final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); + + // setup variables for tracking important facts required for loan + // schedule generation. + + final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); + LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType()) + ? loanApplicationTerms.getExpectedDisbursementDate() + : loanApplicationTerms.getSubmittedOnDate(); + + LoanScheduleParams scheduleParams = LoanScheduleParams.createLoanScheduleParams(currency, + Money.of(currency, chargesDueAtTimeOfDisbursement), periodStartDate, getPrincipalToBeScheduled(loanApplicationTerms)); + + List periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, scheduleParams, + chargesDueAtTimeOfDisbursement); + + boolean isFirstRepayment = true; + + if (loanApplicationTerms.isMultiDisburseLoan()) { + /* fetches the first tranche amount and also updates other tranche details to map */ + BigDecimal disburseAmt = getDisbursementAmount(loanApplicationTerms, scheduleParams.getPeriodStartDate(), + scheduleParams.getDisburseDetailMap(), scheduleParams.applyInterestRecalculation()); + BigDecimal downPaymentAmt = BigDecimal.ZERO; + if (loanApplicationTerms.isDownPaymentEnabled()) { + downPaymentAmt = MathUtil.percentageOf(disburseAmt, loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19); + if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) { + downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, loanApplicationTerms.getInstallmentAmountInMultiplesOf()); + } + } + BigDecimal remainingPrincipalAmt = disburseAmt.subtract(downPaymentAmt); + scheduleParams.setPrincipalToBeScheduled(Money.of(currency, remainingPrincipalAmt)); + loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().zero().plus(remainingPrincipalAmt)); + scheduleParams.setOutstandingBalance(Money.of(currency, remainingPrincipalAmt)); + scheduleParams.setOutstandingBalanceAsPerRest(Money.of(currency, remainingPrincipalAmt)); + loanApplicationTerms.resetFixedEmiAmount(); + } + + // charges which depends on total loan interest will be added to this + // set and handled separately after all installments generated + final Set nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges); + boolean isNextRepaymentAvailable = true; + + while (!scheduleParams.getOutstandingBalance().isZero()) { + scheduleParams.setActualRepaymentDate(getScheduledDateGenerator() + .generateNextRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment)); + AdjustedDateDetailsDTO adjustedDateDetailsDTO = getScheduledDateGenerator() + .adjustRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, holidayDetailDTO); + scheduleParams.setActualRepaymentDate(adjustedDateDetailsDTO.getChangedActualRepaymentDate()); + isFirstRepayment = false; + LocalDate scheduledDueDate = adjustedDateDetailsDTO.getChangedScheduleDate(); + + // this block is to generate the schedule till the specified + // date(used for calculating preclosure) + boolean isCompletePeriod = true; + if (scheduleParams.getScheduleTillDate() != null && !scheduledDueDate.isBefore(scheduleParams.getScheduleTillDate())) { + if (!scheduledDueDate.isEqual(scheduleParams.getScheduleTillDate())) { + isCompletePeriod = false; + } + scheduledDueDate = scheduleParams.getScheduleTillDate(); + isNextRepaymentAvailable = false; + } + + ScheduleCurrentPeriodParams currentPeriodParams = new ScheduleCurrentPeriodParams(currency, BigDecimal.ZERO); + + if (loanApplicationTerms.isMultiDisburseLoan()) { + processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, scheduledDueDate); + } + + if (currentPeriodParams.isSkipCurrentLoop()) { + continue; + } + + // 5 determine principal,interest of repayment period + PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( + getPaymentPeriodsInOneYearCalculator(), currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), + scheduleParams.getTotalCumulativePrincipal().minus(scheduleParams.getReducePrincipal()), + scheduleParams.getTotalCumulativeInterest(), loanApplicationTerms.getTotalInterestDue(), + scheduleParams.getTotalOutstandingInterestPaymentDueToGrace(), scheduleParams.getOutstandingBalanceAsPerRest(), + loanApplicationTerms, scheduleParams.getPeriodNumber(), mc, null, scheduleParams.getCompoundingMap(), null, + scheduledDueDate, null); + + // update cumulative fields for principal + currentPeriodParams.setPrincipalForThisPeriod(principalInterestForThisPeriod.principal()); + updateOutstandingBalance(scheduleParams, currentPeriodParams); + + if (scheduleParams.getOutstandingBalance().isLessThanZero() || !isNextRepaymentAvailable) { + currentPeriodParams.plusPrincipalForThisPeriod(scheduleParams.getOutstandingBalance()); + scheduleParams.setOutstandingBalance(Money.zero(currency)); + } + + if (!isNextRepaymentAvailable) { + scheduleParams.getDisburseDetailMap().clear(); + } + + // applies charges for the period + applyChargesForCurrentPeriod(loanCharges, currency, scheduleParams, scheduledDueDate, currentPeriodParams, mc); + + // sum up real totalInstallmentDue from components + final Money totalInstallmentDue = currentPeriodParams.fetchTotalAmountForPeriod(); + + // if previous installment is last then add interest to same + // installment + if (currentPeriodParams.getLastInstallment() != null && currentPeriodParams.getPrincipalForThisPeriod().isZero()) { + currentPeriodParams.getLastInstallment().addInterestAmount(currentPeriodParams.getInterestForThisPeriod()); + continue; + } + + // create repayment period from parts + LoanScheduleModelPeriod installment = LoanScheduleModelRepaymentPeriod.repayment(scheduleParams.getInstalmentNumber(), + scheduleParams.getPeriodStartDate(), scheduledDueDate, currentPeriodParams.getPrincipalForThisPeriod(), + scheduleParams.getOutstandingBalance(), currentPeriodParams.getInterestForThisPeriod(), + currentPeriodParams.getFeeChargesForInstallment(), currentPeriodParams.getPenaltyChargesForInstallment(), + totalInstallmentDue, !isCompletePeriod); + if (principalInterestForThisPeriod.getRescheduleInterestPortion() != null) { + installment.setRescheduleInterestPortion(principalInterestForThisPeriod.getRescheduleInterestPortion().getAmount()); + } + addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment); + + if (loanApplicationTerms.getCurrentPeriodFixedEmiAmount() != null) { + installment.setEMIFixedSpecificToInstallmentTrue(); + } + + periods.add(installment); + + // Updates principal paid map with efective date for reducing + // the amount from outstanding balance(interest calculation) + updateAmountsWithEffectiveDate(loanApplicationTerms, holidayDetailDTO, scheduleParams, scheduledDueDate, currentPeriodParams, + installment); + + // handle cumulative fields + + scheduleParams.addTotalCumulativePrincipal(currentPeriodParams.getPrincipalForThisPeriod()); + scheduleParams.addTotalRepaymentExpected(totalInstallmentDue); + scheduleParams.addTotalCumulativeInterest(currentPeriodParams.getInterestForThisPeriod()); + scheduleParams.setPeriodStartDate(scheduledDueDate); + scheduleParams.incrementInstalmentNumber(); + scheduleParams.incrementPeriodNumber(); + // if (termVariationParams.isRecalculateAmounts()) { + // loanApplicationTerms.setCurrentPeriodFixedEmiAmount(null); + // loanApplicationTerms.setCurrentPeriodFixedPrincipalAmount(null); + // adjustInstallmentOrPrincipalAmount(loanApplicationTerms, scheduleParams.getTotalCumulativePrincipal(), + // scheduleParams.getPeriodNumber(), mc); + // } + } + + // determine fees and penalties for charges which depends on total + // loan interest + updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc); + + final BigDecimal totalPrincipalPaid = BigDecimal.ZERO; + final BigDecimal totalOutstanding = BigDecimal.ZERO; + + return LoanScheduleModel.from(periods, applicationCurrency, scheduleParams.getLoanTermInDays(), + scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativePrincipal().getAmount(), totalPrincipalPaid, + scheduleParams.getTotalCumulativeInterest().getAmount(), scheduleParams.getTotalFeeChargesCharged().getAmount(), + scheduleParams.getTotalPenaltyChargesCharged().getAmount(), scheduleParams.getTotalRepaymentExpected().getAmount(), + totalOutstanding); + } + + @Override + public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan, + HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, + LocalDate rescheduleFrom) { + return null; + } + + @Override + public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, + LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, + LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { + return null; + } + + // Abstract methods + public abstract ScheduledDateGenerator getScheduledDateGenerator(); + + public abstract PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator(); + + public abstract PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator, + BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest, + Money totalInterestDueForLoan, Money cumulatingInterestPaymentDueToGrace, Money outstandingBalance, + LoanApplicationTerms loanApplicationTerms, int periodNumber, MathContext mc, TreeMap principalVariation, + Map compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate, + Collection termVariations); + + // Private, internal methods + private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set loanCharges) { + BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO; + for (final LoanCharge loanCharge : loanCharges) { + if (loanCharge.isDueAtDisbursement()) { + chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount()); + } + } + return chargesDueAtTimeOfDisbursement; + } + + /** + * this method calculates the principal amount for generating the repayment schedule. + */ + private Money getPrincipalToBeScheduled(final LoanApplicationTerms loanApplicationTerms) { + Money principalToBeScheduled; + if (loanApplicationTerms.isMultiDisburseLoan()) { + if (loanApplicationTerms.getTotalDisbursedAmount().isGreaterThanZero()) { + principalToBeScheduled = loanApplicationTerms.getTotalMultiDisbursedAmount(); + } else if (loanApplicationTerms.getApprovedPrincipal().isGreaterThanZero()) { + principalToBeScheduled = loanApplicationTerms.getApprovedPrincipal(); + } else { + principalToBeScheduled = loanApplicationTerms.getPrincipal(); + } + } else { + principalToBeScheduled = loanApplicationTerms.getPrincipal(); + } + return principalToBeScheduled; + } + + private List createNewLoanScheduleListWithDisbursementDetails(final LoanApplicationTerms loanApplicationTerms, + final LoanScheduleParams loanScheduleParams, final BigDecimal chargesDueAtTimeOfDisbursement) { + List periods = new ArrayList<>(); + + // In case of `disallowExpectedDisbursementDetails = true`, anyway at least 1 disbursement details must exist + if (loanApplicationTerms.getDisbursementDatas().isEmpty()) { + loanApplicationTerms.getDisbursementDatas() + .add(new DisbursementData(1L, loanApplicationTerms.getExpectedDisbursementDate(), + loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getPrincipal().getAmount(), null, null, + null, null)); + } + for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) { + if (disbursementData.disbursementDate().equals(loanScheduleParams.getPeriodStartDate())) { + final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement( + disbursementData.disbursementDate(), Money.of(loanScheduleParams.getCurrency(), disbursementData.getPrincipal()), + chargesDueAtTimeOfDisbursement); + periods.add(disbursementPeriod); + if (loanApplicationTerms.isDownPaymentEnabled()) { + final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms, + loanScheduleParams, loanApplicationTerms.getExpectedDisbursementDate(), disbursementData.getPrincipal()); + periods.add(downPaymentPeriod); + } + } + } + + return periods; + } + + private LoanScheduleModelDownPaymentPeriod createDownPaymentPeriod(LoanApplicationTerms loanApplicationTerms, + LoanScheduleParams scheduleParams, LocalDate date, BigDecimal periodBaseAmount) { + BigDecimal downPaymentAmount = MathUtil.percentageOf(periodBaseAmount, + loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19); + if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) { + downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, loanApplicationTerms.getInstallmentAmountInMultiplesOf()); + } + Money downPayment = Money.of(loanApplicationTerms.getCurrency(), downPaymentAmount); + LoanScheduleModelDownPaymentPeriod installment = LoanScheduleModelDownPaymentPeriod + .downPayment(scheduleParams.getInstalmentNumber(), date, downPayment, scheduleParams.getOutstandingBalance()); + + addLoanRepaymentScheduleInstallment(scheduleParams.getInstallments(), installment); + + scheduleParams.incrementInstalmentNumber(); + scheduleParams.addTotalRepaymentExpected(downPayment); + + return installment; + } + + private LoanRepaymentScheduleInstallment addLoanRepaymentScheduleInstallment(final List installments, + final LoanScheduleModelPeriod scheduledLoanInstallment) { + LoanRepaymentScheduleInstallment installment = null; + if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) { + installment = new LoanRepaymentScheduleInstallment(null, scheduledLoanInstallment.periodNumber(), + scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(), + scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.interestDue(), + scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.penaltyChargesDue(), + scheduledLoanInstallment.isRecalculatedInterestComponent(), scheduledLoanInstallment.getLoanCompoundingDetails(), + scheduledLoanInstallment.rescheduleInterestPortion(), scheduledLoanInstallment.isDownPaymentPeriod()); + installments.add(installment); + } + return installment; + } + + /** + * Method add extra disbursement periods (if applicable) and update the schedule params + */ + private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement, + LoanScheduleParams scheduleParams, final Collection periods, final LocalDate scheduledDueDate) { + for (Map.Entry disburseDetail : scheduleParams.getDisburseDetailMap().entrySet()) { + if (disburseDetail.getKey().isAfter(scheduleParams.getPeriodStartDate()) + && !disburseDetail.getKey().isAfter(scheduledDueDate)) { + // validation check for amount not exceeds specified max + // amount as per the configuration + loanApplicationTerms.getMaxOutstandingBalance(); + if (scheduleParams.getOutstandingBalance().plus(disburseDetail.getValue()) + .isGreaterThan(loanApplicationTerms.getMaxOutstandingBalance())) { + String errorMsg = "Outstanding balance must not exceed the amount: " + loanApplicationTerms.getMaxOutstandingBalance(); + throw new MultiDisbursementOutstandingAmoutException(errorMsg, + loanApplicationTerms.getMaxOutstandingBalance().getAmount(), disburseDetail.getValue()); + } + + // creates and add disbursement detail to the repayments + // period + final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod + .disbursement(disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement); + periods.add(disbursementPeriod); + + BigDecimal downPaymentAmt = BigDecimal.ZERO; + if (loanApplicationTerms.isDownPaymentEnabled()) { + final LoanScheduleModelDownPaymentPeriod downPaymentPeriod = createDownPaymentPeriod(loanApplicationTerms, + scheduleParams, disburseDetail.getKey(), disburseDetail.getValue().getAmount()); + periods.add(downPaymentPeriod); + downPaymentAmt = downPaymentPeriod.principalDue(); + } + // updates actual outstanding balance with new + // disbursement detail + Money remainingPrincipal = disburseDetail.getValue().minus(downPaymentAmt); + scheduleParams.addOutstandingBalance(remainingPrincipal); + scheduleParams.addOutstandingBalanceAsPerRest(remainingPrincipal); + scheduleParams.addPrincipalToBeScheduled(remainingPrincipal); + loanApplicationTerms.setPrincipal(loanApplicationTerms.getPrincipal().plus(remainingPrincipal)); + loanApplicationTerms.resetFixedEmiAmount(); + } + } + } + + private void applyChargesForCurrentPeriod(final Set loanCharges, final MonetaryCurrency currency, + LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams, + final MathContext mc) { + PrincipalInterest principalInterest = new PrincipalInterest(currentPeriodParams.getPrincipalForThisPeriod(), + currentPeriodParams.getInterestForThisPeriod(), null); + currentPeriodParams.setFeeChargesForInstallment(cumulativeFeeChargesDueWithin(scheduleParams.getPeriodStartDate(), scheduledDueDate, + loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), + scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc)); + currentPeriodParams.setPenaltyChargesForInstallment(cumulativePenaltyChargesDueWithin(scheduleParams.getPeriodStartDate(), + scheduledDueDate, loanCharges, currency, principalInterest, scheduleParams.getPrincipalToBeScheduled(), + scheduleParams.getTotalCumulativeInterest(), true, scheduleParams.isFirstPeriod(), mc)); + scheduleParams.addTotalFeeChargesCharged(currentPeriodParams.getFeeChargesForInstallment()); + scheduleParams.addTotalPenaltyChargesCharged(currentPeriodParams.getPenaltyChargesForInstallment()); + } + + private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, + final MonetaryCurrency monetaryCurrency, final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, + final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, + final MathContext mc) { + Money cumulative = Money.zero(monetaryCurrency); + for (final LoanCharge loanCharge : loanCharges) { + if (!loanCharge.isDueAtDisbursement() && loanCharge.isFeeCharge()) { + cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed, + totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc); + } + } + return cumulative; + } + + private Money getCumulativeAmountOfCharge(LocalDate periodStart, LocalDate periodEnd, PrincipalInterest principalInterestForThisPeriod, + Money principalDisbursed, Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, + boolean isFirstPeriod, LoanCharge loanCharge, Money cumulative, MathContext mc) { + boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) + : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); + if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { + cumulative = calculateInstallmentCharge(principalInterestForThisPeriod, cumulative, loanCharge, mc); + } else if (loanCharge.isOverdueInstallmentCharge() && isDue && loanCharge.getChargeCalculation().isPercentageBased()) { + cumulative = cumulative.plus(loanCharge.chargeAmount()); + } else if (isDue && loanCharge.getChargeCalculation().isPercentageBased()) { + cumulative = calculateSpecificDueDateChargeWithPercentage(principalDisbursed, totalInterestChargedForFullLoanTerm, cumulative, + loanCharge, mc); + } else if (isDue) { + cumulative = cumulative.plus(loanCharge.amount()); + } + return cumulative; + } + + private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, + final Set loanCharges, final MonetaryCurrency monetaryCurrency, + final PrincipalInterest principalInterestForThisPeriod, final Money principalDisbursed, + final Money totalInterestChargedForFullLoanTerm, boolean isInstallmentChargeApplicable, final boolean isFirstPeriod, + final MathContext mc) { + Money cumulative = Money.zero(monetaryCurrency); + for (final LoanCharge loanCharge : loanCharges) { + if (loanCharge.isPenaltyCharge()) { + cumulative = getCumulativeAmountOfCharge(periodStart, periodEnd, principalInterestForThisPeriod, principalDisbursed, + totalInterestChargedForFullLoanTerm, isInstallmentChargeApplicable, isFirstPeriod, loanCharge, cumulative, mc); + } + } + return cumulative; + } + + private Money calculateInstallmentCharge(final PrincipalInterest principalInterestForThisPeriod, Money cumulative, + final LoanCharge loanCharge, final MathContext mc) { + if (loanCharge.getChargeCalculation().isPercentageBased()) { + BigDecimal amount = BigDecimal.ZERO; + if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { + amount = amount.add(principalInterestForThisPeriod.principal().getAmount()) + .add(principalInterestForThisPeriod.interest().getAmount()); + } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { + amount = amount.add(principalInterestForThisPeriod.interest().getAmount()); + } else { + amount = amount.add(principalInterestForThisPeriod.principal().getAmount()); + } + BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc); + cumulative = cumulative.plus(loanChargeAmt); + } else { + cumulative = cumulative.plus(loanCharge.amountOrPercentage()); + } + return cumulative; + } + + private Money calculateSpecificDueDateChargeWithPercentage(final Money principalDisbursed, + final Money totalInterestChargedForFullLoanTerm, Money cumulative, final LoanCharge loanCharge, final MathContext mc) { + BigDecimal amount = BigDecimal.ZERO; + if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { + amount = amount.add(principalDisbursed.getAmount()).add(totalInterestChargedForFullLoanTerm.getAmount()); + } else if (loanCharge.getChargeCalculation().isPercentageOfInterest()) { + amount = amount.add(totalInterestChargedForFullLoanTerm.getAmount()); + } else { + amount = amount.add(principalDisbursed.getAmount()); + } + BigDecimal loanChargeAmt = amount.multiply(loanCharge.getPercentage()).divide(BigDecimal.valueOf(100), mc); + cumulative = cumulative.plus(loanChargeAmt); + return cumulative; + } + + private void updateAmountsWithEffectiveDate(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO, + LoanScheduleParams scheduleParams, LocalDate scheduledDueDate, ScheduleCurrentPeriodParams currentPeriodParams, + LoanScheduleModelPeriod installment) { + LocalDate amountApplicableDate = installment.periodDueDate(); + if (loanApplicationTerms.isInterestRecalculationEnabled()) { + amountApplicableDate = getNextRestScheduleDate(installment.periodDueDate().minusDays(1), loanApplicationTerms, + holidayDetailDTO); + } + updateMapWithAmount(scheduleParams.getPrincipalPortionMap(), + currentPeriodParams.getPrincipalForThisPeriod().minus(currentPeriodParams.getReducedBalance()), amountApplicableDate); + + // update outstanding balance for interest calculation + updateOutstandingBalanceAsPerRest(scheduleParams, scheduledDueDate); + } + + private LocalDate getNextRestScheduleDate(LocalDate startDate, LoanApplicationTerms loanApplicationTerms, + final HolidayDetailDTO holidayDetailDTO) { + LocalDate nextScheduleDate; + if (loanApplicationTerms.getRecalculationFrequencyType().isSameAsRepayment()) { + nextScheduleDate = getScheduledDateGenerator().generateNextScheduleDateStartingFromDisburseDateOrRescheduleDate(startDate, + loanApplicationTerms, holidayDetailDTO); + } else { + CalendarInstance calendarInstance = loanApplicationTerms.getRestCalendarInstance(); + nextScheduleDate = CalendarUtils.getNextScheduleDate(calendarInstance.getCalendar(), startDate); + } + + return nextScheduleDate; + } + + private void updateMapWithAmount(final Map map, final Money amount, final LocalDate amountApplicableDate) { + Money principalPaid = amount; + if (map.containsKey(amountApplicableDate)) { + principalPaid = map.get(amountApplicableDate).plus(principalPaid); + } + map.put(amountApplicableDate, principalPaid); + + } + + private void updateOutstandingBalanceAsPerRest(final LoanScheduleParams scheduleParams, final LocalDate scheduledDueDate) { + scheduleParams.setOutstandingBalanceAsPerRest(updateBalanceForInterestCalculation(scheduleParams.getPrincipalPortionMap(), + scheduledDueDate, scheduleParams.getOutstandingBalanceAsPerRest(), false)); + } + + /** + * Identifies all the past date principal changes and apply them on outstanding balance for future calculations + */ + private Money updateBalanceForInterestCalculation(final Map principalPortionMap, final LocalDate scheduledDueDate, + final Money outstandingBalanceAsPerRest, boolean addMapDetails) { + List removeFromPrincipalPortionMap = new ArrayList<>(); + Money outstandingBalance = outstandingBalanceAsPerRest; + for (Map.Entry principal : principalPortionMap.entrySet()) { + if (!principal.getKey().isAfter(scheduledDueDate)) { + if (addMapDetails) { + outstandingBalance = outstandingBalance.plus(principal.getValue()); + } else { + outstandingBalance = outstandingBalance.minus(principal.getValue()); + } + removeFromPrincipalPortionMap.add(principal.getKey()); + } + } + for (LocalDate date : removeFromPrincipalPortionMap) { + principalPortionMap.remove(date); + } + return outstandingBalance; + } + + private void updatePeriodsWithCharges(final MonetaryCurrency currency, LoanScheduleParams scheduleParams, + final Collection periods, final Set nonCompoundingCharges, MathContext mc) { + for (LoanScheduleModelPeriod loanScheduleModelPeriod : periods) { + if (loanScheduleModelPeriod.isRepaymentPeriod()) { + PrincipalInterest principalInterest = new PrincipalInterest(Money.of(currency, loanScheduleModelPeriod.principalDue()), + Money.of(currency, loanScheduleModelPeriod.interestDue()), null); + Money feeChargesForInstallment = cumulativeFeeChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), + loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest, + scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(), + !loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc); + Money penaltyChargesForInstallment = cumulativePenaltyChargesDueWithin(loanScheduleModelPeriod.periodFromDate(), + loanScheduleModelPeriod.periodDueDate(), nonCompoundingCharges, currency, principalInterest, + scheduleParams.getPrincipalToBeScheduled(), scheduleParams.getTotalCumulativeInterest(), + !loanScheduleModelPeriod.isRecalculatedInterestComponent(), scheduleParams.isFirstPeriod(), mc); + scheduleParams.addTotalFeeChargesCharged(feeChargesForInstallment); + scheduleParams.addTotalPenaltyChargesCharged(penaltyChargesForInstallment); + scheduleParams.addTotalRepaymentExpected(feeChargesForInstallment.plus(penaltyChargesForInstallment)); + loanScheduleModelPeriod.addLoanCharges(feeChargesForInstallment.getAmount(), penaltyChargesForInstallment.getAmount()); + } + } + } + + private Set separateTotalCompoundingPercentageCharges(final Set loanCharges) { + Set interestCharges = new HashSet<>(); + for (final LoanCharge loanCharge : loanCharges) { + if (loanCharge.isSpecifiedDueDate() && (loanCharge.getChargeCalculation().isPercentageOfInterest() + || loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest())) { + interestCharges.add(loanCharge); + } + } + loanCharges.removeAll(interestCharges); + return interestCharges; + } + + private BigDecimal getDisbursementAmount(final LoanApplicationTerms loanApplicationTerms, LocalDate disbursementDate, + final Map disburseDetails, final boolean excludePastUnDisbursed) { + + // this method relates to multi-disbursement loans + BigDecimal principal = BigDecimal.ZERO; + if (loanApplicationTerms.getDisbursementDatas().size() == 0) { + // non tranche loans have no disbursement data entries in submitted and approved status + // the appropriate approved amount or applied for amount is used to show a proposed schedule + if (loanApplicationTerms.getApprovedPrincipal().getAmount().compareTo(BigDecimal.ZERO) > 0) { + principal = loanApplicationTerms.getApprovedPrincipal().getAmount(); + } else { + principal = loanApplicationTerms.getPrincipal().getAmount(); + } + } else { + MonetaryCurrency currency = loanApplicationTerms.getPrincipal().getCurrency(); + for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) { + if (disbursementData.disbursementDate().equals(disbursementDate)) { + principal = principal.add(disbursementData.getPrincipal()); + } else if (!excludePastUnDisbursed || disbursementData.isDisbursed() + || !disbursementData.disbursementDate().isBefore(DateUtils.getBusinessLocalDate())) { + /* + * JW: sums up amounts by disbursal date in case of side-effect issues. Original assumed that there + * were no duplicate disbursal dates and 'put' each amount into the map keyed by date + */ + Money previousSum = disburseDetails.get(disbursementData.disbursementDate()); + BigDecimal sumToNow = BigDecimal.ZERO; + if (previousSum != null) { + sumToNow = previousSum.getAmount(); + } + sumToNow = sumToNow.add(disbursementData.getPrincipal()); + disburseDetails.put(disbursementData.disbursementDate(), Money.of(currency, sumToNow)); + } + } + } + return principal; + } + + private void updateOutstandingBalance(LoanScheduleParams scheduleParams, ScheduleCurrentPeriodParams currentPeriodParams) { + // update outstandingLoanBlance using current period + // 'principalDue' + scheduleParams + .reduceOutstandingBalance(currentPeriodParams.getPrincipalForThisPeriod().minus(currentPeriodParams.getReducedBalance())); + } +} 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/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java similarity index 92% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DecliningBalanceInterestLoanScheduleGenerator.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java index 64b474f1f..53c0cbe2c 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/CumulativeDecliningBalanceInterestLoanScheduleGenerator.java @@ -26,10 +26,12 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; +import org.springframework.stereotype.Component; /** *

@@ -54,11 +56,26 @@ import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; * minus interest due. *

*/ -public class DecliningBalanceInterestLoanScheduleGenerator extends AbstractLoanScheduleGenerator { +@Component +@RequiredArgsConstructor +public class CumulativeDecliningBalanceInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator { + + private final ScheduledDateGenerator scheduledDateGenerator; + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator; + + @Override + public ScheduledDateGenerator getScheduledDateGenerator() { + return scheduledDateGenerator; + } + + @Override + public PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator() { + return paymentPeriodsInOneYearCalculator; + } @Override public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final PaymentPeriodsInOneYearCalculator calculator, - final double interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal, + final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal, @SuppressWarnings("unused") final Money totalCumulativeInterest, @SuppressWarnings("unused") final Money totalInterestDueForLoan, final Money cumulatingInterestPaymentDueToGrace, final Money outstandingBalance, final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final MathContext mc, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java similarity index 82% rename from fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java rename to fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java index 78f2ebcad..852bd9b12 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/FlatInterestLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/CumulativeFlatInterestLoanScheduleGenerator.java @@ -18,19 +18,37 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; import java.util.Collection; import java.util.Map; import java.util.TreeMap; +import lombok.RequiredArgsConstructor; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.springframework.stereotype.Component; -public class FlatInterestLoanScheduleGenerator extends AbstractLoanScheduleGenerator { +@Component +@RequiredArgsConstructor +public class CumulativeFlatInterestLoanScheduleGenerator extends AbstractCumulativeLoanScheduleGenerator { + + private final ScheduledDateGenerator scheduledDateGenerator; + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator; + + @Override + public ScheduledDateGenerator getScheduledDateGenerator() { + return scheduledDateGenerator; + } + + @Override + public PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator() { + return paymentPeriodsInOneYearCalculator; + } @Override public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final PaymentPeriodsInOneYearCalculator calculator, - final double interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal, + final BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, final Money totalCumulativePrincipal, Money totalCumulativeInterest, Money totalInterestDueForLoan, final Money cumulatingInterestPaymentDueToGrace, final Money outstandingBalance, final LoanApplicationTerms loanApplicationTerms, final int periodNumber, final MathContext mc, @SuppressWarnings("unused") TreeMap principalVariation, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java index fdad36ea5..46ea6543a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultLoanScheduleGeneratorFactory.java @@ -18,29 +18,36 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import lombok.RequiredArgsConstructor; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class DefaultLoanScheduleGeneratorFactory implements LoanScheduleGeneratorFactory { + private final ProgressiveLoanScheduleGenerator progressiveLoanScheduleGenerator; + private final CumulativeFlatInterestLoanScheduleGenerator cumulativeFlatInterestLoanScheduleGenerator; + private final CumulativeDecliningBalanceInterestLoanScheduleGenerator cumulativeDecliningBalanceInterestLoanScheduleGenerator; + @Override - public LoanScheduleGenerator create(final InterestMethod interestMethod) { + public LoanScheduleGenerator create(final LoanScheduleType loanScheduleType, final InterestMethod interestMethod) { + return switch (loanScheduleType) { + case CUMULATIVE -> cumulativeLoanScheduleGenerator(interestMethod); + case PROGRESSIVE -> progressiveLoanScheduleGenerator(interestMethod); + }; + } - LoanScheduleGenerator loanScheduleGenerator = null; + private LoanScheduleGenerator cumulativeLoanScheduleGenerator(final InterestMethod interestMethod) { + return switch (interestMethod) { + case FLAT -> cumulativeFlatInterestLoanScheduleGenerator; + case DECLINING_BALANCE -> cumulativeDecliningBalanceInterestLoanScheduleGenerator; + case INVALID -> null; + }; + } - switch (interestMethod) { - case FLAT: - loanScheduleGenerator = new FlatInterestLoanScheduleGenerator(); - break; - case DECLINING_BALANCE: - loanScheduleGenerator = new DecliningBalanceInterestLoanScheduleGenerator(); - break; - case INVALID: - break; - } - - return loanScheduleGenerator; + private LoanScheduleGenerator progressiveLoanScheduleGenerator(final InterestMethod interestMethod) { + return progressiveLoanScheduleGenerator; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java index 0af3bf6c7..4c7cc9464 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultPaymentPeriodsInOneYearCalculator.java @@ -18,90 +18,83 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import java.math.BigDecimal; +import java.math.MathContext; import java.time.LocalDate; import java.time.temporal.ChronoUnit; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.domain.LocalDateInterval; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +@Component +@Slf4j public class DefaultPaymentPeriodsInOneYearCalculator implements PaymentPeriodsInOneYearCalculator { - private static final Logger LOG = LoggerFactory.getLogger(DefaultPaymentPeriodsInOneYearCalculator.class); - @Override public Integer calculate(final PeriodFrequencyType repaymentFrequencyType) { - Integer paymentPeriodsInOneYear = Integer.valueOf(0); + Integer paymentPeriodsInOneYear = 0; switch (repaymentFrequencyType) { case DAYS: - paymentPeriodsInOneYear = Integer.valueOf(365); + paymentPeriodsInOneYear = 365; break; case WEEKS: - paymentPeriodsInOneYear = Integer.valueOf(52); + paymentPeriodsInOneYear = 52; break; case MONTHS: - paymentPeriodsInOneYear = Integer.valueOf(12); + paymentPeriodsInOneYear = 12; break; case YEARS: - paymentPeriodsInOneYear = Integer.valueOf(1); + paymentPeriodsInOneYear = 1; break; case INVALID: - paymentPeriodsInOneYear = Integer.valueOf(0); + paymentPeriodsInOneYear = 0; break; case WHOLE_TERM: - LOG.error("TODO Implement repaymentFrequencyType for WHOLE_TERM"); + log.error("TODO Implement repaymentFrequencyType for WHOLE_TERM"); break; } return paymentPeriodsInOneYear; } @Override - public double calculatePortionOfRepaymentPeriodInterestChargingGrace(final LocalDate repaymentPeriodStartDate, + public BigDecimal calculatePortionOfRepaymentPeriodInterestChargingGrace(final LocalDate repaymentPeriodStartDate, final LocalDate scheduledDueDate, final LocalDate interestChargedFromLocalDate, - final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer repaidEvery) { + final PeriodFrequencyType repaymentPeriodFrequencyType, final int repaidEvery, MathContext mc) { - Double periodFraction = Double.valueOf("0.0"); + BigDecimal periodFraction = BigDecimal.ZERO; final LocalDateInterval repaymentPeriod = new LocalDateInterval(repaymentPeriodStartDate, scheduledDueDate); if (interestChargedFromLocalDate != null && repaymentPeriod.fallsBefore(interestChargedFromLocalDate.plusDays(1))) { - periodFraction = Double.valueOf("1.0"); + periodFraction = BigDecimal.ONE; } else if (interestChargedFromLocalDate != null && repaymentPeriod.contains(interestChargedFromLocalDate)) { final int numberOfDaysInterestCalculationGraceInPeriod = Math .toIntExact(ChronoUnit.DAYS.between(repaymentPeriodStartDate, interestChargedFromLocalDate)); periodFraction = calculateRepaymentPeriodFraction(repaymentPeriodFrequencyType, repaidEvery, - numberOfDaysInterestCalculationGraceInPeriod); + numberOfDaysInterestCalculationGraceInPeriod, mc); } return periodFraction; } - private double calculateRepaymentPeriodFraction(final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer every, - final Integer numberOfDaysInterestCalculationGrace) { + private BigDecimal calculateRepaymentPeriodFraction(final PeriodFrequencyType repaymentPeriodFrequencyType, final int repaidEvery, + final int numberOfDaysInterestCalculationGrace, final MathContext mc) { - Double fraction = Double.valueOf("0"); - switch (repaymentPeriodFrequencyType) { - case DAYS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() * every.doubleValue(); - break; - case WEEKS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.parseDouble("7.0") * every.doubleValue()); - break; - case MONTHS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.parseDouble("30.0") * every.doubleValue()); - break; - case YEARS: - fraction = numberOfDaysInterestCalculationGrace.doubleValue() / (Double.parseDouble("365.0") * every.doubleValue()); - break; - case INVALID: - fraction = Double.valueOf("0"); - break; - case WHOLE_TERM: - LOG.error("TODO Implement repaymentPeriodFrequencyType for WHOLE_TERM"); - break; - } - return fraction; + BigDecimal repayEveryBD = BigDecimal.valueOf(repaidEvery); + BigDecimal noDaysInterestCalculationGrace = BigDecimal.valueOf(numberOfDaysInterestCalculationGrace); + return switch (repaymentPeriodFrequencyType) { + case DAYS -> noDaysInterestCalculationGrace.multiply(repayEveryBD, mc); + case WEEKS -> noDaysInterestCalculationGrace.divide(BigDecimal.valueOf(7), mc).multiply(repayEveryBD, mc); + case MONTHS -> noDaysInterestCalculationGrace.divide(BigDecimal.valueOf(30), mc).multiply(repayEveryBD, mc); + case YEARS -> noDaysInterestCalculationGrace.divide(BigDecimal.valueOf(365), mc).multiply(repayEveryBD, mc); + case WHOLE_TERM -> { + log.error("TODO Implement repaymentPeriodFrequencyType for WHOLE_TERM"); + yield BigDecimal.ZERO; + } + default -> BigDecimal.ZERO; + }; } } 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 6a646a3db..72e529af0 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 @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; import java.time.LocalDate; import java.time.temporal.ChronoUnit; +import lombok.extern.slf4j.Slf4j; import net.fortuna.ical4j.model.Recur; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.holiday.domain.Holiday; @@ -34,13 +35,12 @@ import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +@Component +@Slf4j public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { - private static final Logger LOG = LoggerFactory.getLogger(DefaultScheduledDateGenerator.class); - @Override public LocalDate getLastRepaymentDate(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) { @@ -240,7 +240,7 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { case INVALID: break; case WHOLE_TERM: - LOG.error("TODO Implement getRepaymentPeriodDate for WHOLE_TERM"); + log.error("TODO Implement getRepaymentPeriodDate for WHOLE_TERM"); break; } return dueRepaymentPeriodDate; @@ -282,7 +282,7 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { case INVALID: break; case WHOLE_TERM: - LOG.error("TODO Implement isDateFallsInSchedule for WHOLE_TERM"); + log.error("TODO Implement isDateFallsInSchedule for WHOLE_TERM"); break; } return isScheduledDate; @@ -319,7 +319,7 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { case INVALID: break; case WHOLE_TERM: - LOG.error("TODO Implement repaymentPeriodFrequencyType for WHOLE_TERM"); + log.error("TODO Implement repaymentPeriodFrequencyType for WHOLE_TERM"); break; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java index 8a5104e9c..3f9894ea9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleModelDisbursementPeriod.java @@ -40,8 +40,7 @@ public final class LoanScheduleModelDisbursementPeriod implements LoanScheduleMo public static LoanScheduleModelDisbursementPeriod disbursement(final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement) { - final int periodNumber = 0; - return new LoanScheduleModelDisbursementPeriod(periodNumber, loanApplicationTerms.getExpectedDisbursementDate(), + return new LoanScheduleModelDisbursementPeriod(null, loanApplicationTerms.getExpectedDisbursementDate(), loanApplicationTerms.getPrincipal(), chargesDueAtTimeOfDisbursement); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanTermVariationParams.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanTermVariationParams.java new file mode 100644 index 000000000..951ba0d36 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanTermVariationParams.java @@ -0,0 +1,9 @@ +package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; + +import java.time.LocalDate; +import java.util.List; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; + +public record LoanTermVariationParams(boolean skipPeriod, boolean recalculateAmounts, LocalDate scheduledDueDate, + List variationsData) { +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java new file mode 100644 index 000000000..99a106e2d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -0,0 +1,67 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.util.Collection; +import java.util.Map; +import java.util.TreeMap; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ProgressiveLoanScheduleGenerator extends AbstractProgressiveLoanScheduleGenerator { + + private final ScheduledDateGenerator scheduledDateGenerator; + private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator; + + @Override + public ScheduledDateGenerator getScheduledDateGenerator() { + return scheduledDateGenerator; + } + + @Override + public PaymentPeriodsInOneYearCalculator getPaymentPeriodsInOneYearCalculator() { + return paymentPeriodsInOneYearCalculator; + } + + @Override + public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(PaymentPeriodsInOneYearCalculator calculator, + BigDecimal interestCalculationGraceOnRepaymentPeriodFraction, Money totalCumulativePrincipal, Money totalCumulativeInterest, + Money totalInterestDueForLoan, Money cumulatingInterestPaymentDueToGrace, Money outstandingBalance, + LoanApplicationTerms loanApplicationTerms, int periodNumber, MathContext mc, TreeMap principalVariation, + Map compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate, + Collection termVariations) { + // TODO: handle interest calculation + Money principalForThisInstallment = loanApplicationTerms.calculateTotalPrincipalForPeriod(calculator, outstandingBalance, + periodNumber, mc, Money.zero(loanApplicationTerms.getCurrency())); + final Money totalCumulativePrincipalToDate = totalCumulativePrincipal.plus(principalForThisInstallment); + // adjust if needed + principalForThisInstallment = loanApplicationTerms.adjustPrincipalIfLastRepaymentPeriod(principalForThisInstallment, + totalCumulativePrincipalToDate, periodNumber); + + return new PrincipalInterest(principalForThisInstallment, Money.zero(loanApplicationTerms.getCurrency()), + Money.zero(loanApplicationTerms.getCurrency())); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduleCurrentPeriodParams.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduleCurrentPeriodParams.java new file mode 100644 index 000000000..da427a4d5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduleCurrentPeriodParams.java @@ -0,0 +1,142 @@ +package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; + +import java.math.BigDecimal; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; + +public class ScheduleCurrentPeriodParams { + + Money earlyPaidAmount; + LoanScheduleModelPeriod lastInstallment; + boolean skipCurrentLoop; + Money interestForThisPeriod; + Money principalForThisPeriod; + Money feeChargesForInstallment; + Money penaltyChargesForInstallment; + // for adjusting outstandingBalances + Money reducedBalance; + boolean isEmiAmountChanged; + BigDecimal interestCalculationGraceOnRepaymentPeriodFraction; + + ScheduleCurrentPeriodParams(final MonetaryCurrency currency) { + this(currency, BigDecimal.ZERO); + } + + ScheduleCurrentPeriodParams(final MonetaryCurrency currency, BigDecimal interestCalculationGraceOnRepaymentPeriodFraction) { + this.earlyPaidAmount = Money.zero(currency); + this.lastInstallment = null; + this.skipCurrentLoop = false; + this.interestForThisPeriod = Money.zero(currency); + this.principalForThisPeriod = Money.zero(currency); + this.reducedBalance = Money.zero(currency); + this.feeChargesForInstallment = Money.zero(currency); + this.penaltyChargesForInstallment = Money.zero(currency); + this.isEmiAmountChanged = false; + this.interestCalculationGraceOnRepaymentPeriodFraction = interestCalculationGraceOnRepaymentPeriodFraction; + } + + public Money getEarlyPaidAmount() { + return this.earlyPaidAmount; + } + + public void plusEarlyPaidAmount(Money earlyPaidAmount) { + this.earlyPaidAmount = this.earlyPaidAmount.plus(earlyPaidAmount); + } + + public void minusEarlyPaidAmount(Money earlyPaidAmount) { + this.earlyPaidAmount = this.earlyPaidAmount.minus(earlyPaidAmount); + } + + public LoanScheduleModelPeriod getLastInstallment() { + return this.lastInstallment; + } + + public void setLastInstallment(LoanScheduleModelPeriod lastInstallment) { + this.lastInstallment = lastInstallment; + } + + public boolean isSkipCurrentLoop() { + return this.skipCurrentLoop; + } + + public void setSkipCurrentLoop(boolean skipCurrentLoop) { + this.skipCurrentLoop = skipCurrentLoop; + } + + public Money getInterestForThisPeriod() { + return this.interestForThisPeriod; + } + + public void setInterestForThisPeriod(Money interestForThisPeriod) { + this.interestForThisPeriod = interestForThisPeriod; + } + + public void minusInterestForThisPeriod(Money interestForThisPeriod) { + this.interestForThisPeriod = this.interestForThisPeriod.minus(interestForThisPeriod); + } + + public Money getPrincipalForThisPeriod() { + return this.principalForThisPeriod; + } + + public void setPrincipalForThisPeriod(Money principalForThisPeriod) { + this.principalForThisPeriod = principalForThisPeriod; + } + + public void plusPrincipalForThisPeriod(Money principalForThisPeriod) { + this.principalForThisPeriod = this.principalForThisPeriod.plus(principalForThisPeriod); + } + + public void minusPrincipalForThisPeriod(Money principalForThisPeriod) { + this.principalForThisPeriod = this.principalForThisPeriod.minus(principalForThisPeriod); + } + + public Money getReducedBalance() { + return this.reducedBalance; + } + + public void setReducedBalance(Money reducedBalance) { + this.reducedBalance = reducedBalance; + } + + public Money getFeeChargesForInstallment() { + return this.feeChargesForInstallment; + } + + public void setFeeChargesForInstallment(Money feeChargesForInstallment) { + this.feeChargesForInstallment = feeChargesForInstallment; + } + + public void minusFeeChargesForInstallment(Money feeChargesForInstallment) { + this.feeChargesForInstallment = this.feeChargesForInstallment.minus(feeChargesForInstallment); + } + + public Money getPenaltyChargesForInstallment() { + return this.penaltyChargesForInstallment; + } + + public void setPenaltyChargesForInstallment(Money penaltyChargesForInstallment) { + this.penaltyChargesForInstallment = penaltyChargesForInstallment; + } + + public void minusPenaltyChargesForInstallment(Money penaltyChargesForInstallment) { + this.penaltyChargesForInstallment = this.penaltyChargesForInstallment.minus(penaltyChargesForInstallment); + } + + public Money fetchTotalAmountForPeriod() { + return this.principalForThisPeriod.plus(interestForThisPeriod).plus(feeChargesForInstallment).plus(penaltyChargesForInstallment); + } + + public boolean isEmiAmountChanged() { + return this.isEmiAmountChanged; + } + + public void setEmiAmountChanged(boolean isEmiAmountChanged) { + this.isEmiAmountChanged = isEmiAmountChanged; + } + + public BigDecimal getInterestCalculationGraceOnRepaymentPeriodFraction() { + return this.interestCalculationGraceOnRepaymentPeriodFraction; + } + +} 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 96b62325d..ee35a790c 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 @@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplica import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; @@ -471,6 +472,7 @@ public class LoanScheduleAssembler { .isDisableScheduleExtensionForDownPayment(); } + LoanScheduleType loanScheduleType = loanProduct.getLoanProductRelatedDetail().getLoanScheduleType(); return LoanApplicationTerms.assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments, repaymentEvery, repaymentPeriodFrequencyType, nthDay, weekDayType, amortizationMethod, interestMethod, interestRatePerPeriod, interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod, @@ -485,7 +487,7 @@ public class LoanScheduleAssembler { allowCompoundingOnEod, isEqualAmortization, isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, - isScheduleExtensionForDownPaymentDisabled); + isScheduleExtensionForDownPaymentDisabled, loanScheduleType); } private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, @@ -666,20 +668,22 @@ public class LoanScheduleAssembler { final MathContext mc = new MathContext(8, roundingMode); HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays); - LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), + loanApplicationTerms.getInterestMethod()); if (loanApplicationTerms.isEqualAmortization()) { if (loanApplicationTerms.getInterestMethod().isDecliningBalance()) { final LoanScheduleGenerator decliningLoanScheduleGenerator = this.loanScheduleFactory - .create(InterestMethod.DECLINING_BALANCE); + .create(loanApplicationTerms.getLoanScheduleType(), InterestMethod.DECLINING_BALANCE); LoanScheduleModel loanSchedule = decliningLoanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO); loanApplicationTerms .updateTotalInterestDue(Money.of(loanApplicationTerms.getCurrency(), loanSchedule.getTotalInterestCharged())); } - loanScheduleGenerator = this.loanScheduleFactory.create(InterestMethod.FLAT); + loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), InterestMethod.FLAT); } else { - loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), + loanApplicationTerms.getInterestMethod()); } return loanScheduleGenerator.generate(mc, loanApplicationTerms, loanCharges, detailDTO); @@ -696,7 +700,8 @@ public class LoanScheduleAssembler { loanApplicationTerms.getExpectedDisbursementDate(), HolidayStatusType.ACTIVE.getValue()); final WorkingDays workingDays = this.workingDaysRepository.findOne(); - final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), + loanApplicationTerms.getInterestMethod()); HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays); return loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, loan, detailDTO, loanRepaymentScheduleTransactionProcessor, rescheduleFrom).getLoanScheduleModel(); @@ -705,7 +710,8 @@ public class LoanScheduleAssembler { public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms, Loan loan, final Long officeId, final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), + loanApplicationTerms.getInterestMethod()); final RoundingMode roundingMode = MoneyHelper.getRoundingMode(); final MathContext mc = new MathContext(8, roundingMode); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanReschedulePreviewPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanReschedulePreviewPlatformServiceImpl.java index 3bb566850..d326f5a7c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanReschedulePreviewPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanReschedulePreviewPlatformServiceImpl.java @@ -131,7 +131,8 @@ public class LoanReschedulePreviewPlatformServiceImpl implements LoanRescheduleP final MathContext mathContext = new MathContext(8, roundingMode); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.loanRepaymentScheduleTransactionProcessorFactory .determineProcessor(loan.transactionProcessingStrategy()); - final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), + loanApplicationTerms.getInterestMethod()); loan.setHelpers(null, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); final LoanScheduleDTO loanSchedule = loanScheduleGenerator.rescheduleNextInstallments(mathContext, loanApplicationTerms, loan, loanApplicationTerms.getHolidayDetailDTO(), loanRepaymentScheduleTransactionProcessor, rescheduleFromDate); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java index 5ee8dd060..7a35d2210 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java @@ -436,7 +436,8 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche final MathContext mathContext = new MathContext(8, roundingMode); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.loanRepaymentScheduleTransactionProcessorFactory .determineProcessor(loan.transactionProcessingStrategy()); - final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod()); + final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), + loanApplicationTerms.getInterestMethod()); final LoanLifecycleStateMachine loanLifecycleStateMachine = null; loan.setHelpers(loanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); final LoanScheduleDTO loanSchedule = loanScheduleGenerator.rescheduleNextInstallments(mathContext, loanApplicationTerms, loan, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java index 68b39629b..bef337cb6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountAutoStarter.java @@ -31,6 +31,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -103,8 +104,8 @@ public class LoanAccountAutoStarter { @Bean @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) - public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor() { - return new AdvancedPaymentScheduleTransactionProcessor(); + public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(LoanUtilService loanUtilService) { + return new AdvancedPaymentScheduleTransactionProcessor(loanUtilService); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 996138ce6..8b8470cbb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -46,6 +46,7 @@ import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; @@ -163,7 +164,8 @@ public final class LoanProductDataValidator { LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.REPAYMENT_START_DATE_TYPE, - LoanProductConstants.DISABLE_SCHEDULE_EXTENSION_FOR_DOWN_PAYMENT, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY)); + LoanProductConstants.DISABLE_SCHEDULE_EXTENSION_FOR_DOWN_PAYMENT, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, + LoanProductConstants.LOAN_SCHEDULE_TYPE)); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, @@ -771,6 +773,10 @@ public final class LoanProductDataValidator { .value(enableInstallmentLevelDelinquency).ignoreIfNull().validateForBooleanValue(); } + final String loanScheduleType = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element); + baseDataValidator.reset().parameter(LoanProductConstants.LOAN_SCHEDULE_TYPE).value(loanScheduleType).ignoreIfNull() + .isOneOfEnumValues(LoanScheduleType.class); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index 279e1dd7f..fc8cba5c7 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -28,6 +28,7 @@ import java.util.Map; import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; public class LoanProductTestBuilder { @@ -151,6 +152,7 @@ public class LoanProductTestBuilder { private boolean enableAutoRepaymentForDownPayment = false; private Integer repaymentStartDateType = null; private boolean disableScheduleExtensionForDownPayment = false; + private LoanScheduleType loanScheduleType = LoanScheduleType.CUMULATIVE; public String build() { final HashMap map = build(null, null); @@ -307,6 +309,7 @@ public class LoanProductTestBuilder { if (disableScheduleExtensionForDownPayment) { map.put("disableScheduleExtensionForDownPayment", disableScheduleExtensionForDownPayment); } + map.put("loanScheduleType", loanScheduleType); return map; } @@ -747,4 +750,8 @@ public class LoanProductTestBuilder { return this; } + public LoanProductTestBuilder withLoanScheduleType(LoanScheduleType loanScheduleType) { + this.loanScheduleType = loanScheduleType; + return this; + } }