FINERACT-1981: Progressive loan schedule handling

This commit is contained in:
Adam Saghy
2023-10-31 08:15:50 +01:00
committed by Arnold Gálovics
parent 71d2e51051
commit 2ea23be9a4
33 changed files with 1326 additions and 480 deletions
@@ -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;
}
}
@@ -129,6 +129,15 @@ public class Money implements Comparable<Money> {
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;
@@ -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,
@@ -1289,7 +1289,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
applyAccurals();
}
public void updateLoanSchedule(final Collection<LoanRepaymentScheduleInstallment> 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<LoanCharge> 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<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
List<LoanTransaction> 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<LoanTransaction> retrieveListOfTransactionsPostDisbursementExcludeAccruals() {
final List<LoanTransaction> 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,
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
@@ -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<LoanScheduleModelPeriod> periods;
private final List<LoanScheduleModelPeriod> 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<LoanScheduleModelPeriod> periods, final ApplicationCurrency applicationCurrency,
public static LoanScheduleModel from(final List<LoanScheduleModelPeriod> 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<LoanScheduleModelPeriod> periods,
public static LoanScheduleModel withOverdueChargeUpdation(final List<LoanScheduleModelPeriod> 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<LoanScheduleModelPeriod> periods,
public static LoanScheduleModel withLoanScheduleModelPeriods(final List<LoanScheduleModelPeriod> 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<LoanScheduleModelPeriod> periods, final ApplicationCurrency applicationCurrency,
private LoanScheduleModel(final List<LoanScheduleModelPeriod> 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<LoanSchedulePeriodData> periodsData = new ArrayList<>();
final List<LoanSchedulePeriodData> 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<LoanScheduleModelPeriod> getPeriods() {
public List<LoanScheduleModelPeriod> getPeriods() {
return this.periods;
}
@@ -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
}
@@ -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);
}
@@ -156,4 +156,7 @@ public interface LoanProductConstants {
String ENABLE_INSTALLMENT_LEVEL_DELINQUENCY = "enableInstallmentLevelDelinquency";
// loan schedule type
String LOAN_SCHEDULE_TYPE = "loanScheduleType";
}
@@ -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> 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();
@@ -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;
}
}
@@ -32,4 +32,5 @@
<include relativeToChangelogFile="true" file="parts/1007_add_loan_product_schedule_extension_for_down_payment_configuration.xml"/>
<include relativeToChangelogFile="true" file="parts/1008_add_loan_installment_delinquency_tag.xml"/>
<include relativeToChangelogFile="true" file="parts/1009_refactor_loan_Installment_delinquency_tag.xml"/>
<include relativeToChangelogFile="true" file="parts/1010_introduce_loan_schedule_type_configuration.xml"/>
</databaseChangeLog>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_product_loan">
<column defaultValue="CUMULATIVE" type="varchar(20)" name="loan_schedule_type">
<constraints nullable="false"/>
</column>
</addColumn>
<addColumn tableName="m_loan">
<column defaultValue="CUMULATIVE" type="varchar(20)" name="loan_schedule_type">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
@@ -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);
}
@@ -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<LoanCharge> 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<LoanScheduleModelPeriod> 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<LoanCharge> 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<LocalDate, Money> principalVariation,
Map<LocalDate, Money> compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate,
Collection<LoanTermVariationsData> termVariations);
// Private, internal methods
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> 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<LoanScheduleModelPeriod> createNewLoanScheduleListWithDisbursementDetails(final LoanApplicationTerms loanApplicationTerms,
final LoanScheduleParams loanScheduleParams, final BigDecimal chargesDueAtTimeOfDisbursement) {
List<LoanScheduleModelPeriod> 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<LoanRepaymentScheduleInstallment> 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<LoanScheduleModelPeriod> periods, final LocalDate scheduledDueDate) {
for (Map.Entry<LocalDate, Money> 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<LoanCharge> 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<LoanCharge> 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<LoanCharge> 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<LocalDate, Money> 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<LocalDate, Money> principalPortionMap, final LocalDate scheduledDueDate,
final Money outstandingBalanceAsPerRest, boolean addMapDetails) {
List<LocalDate> removeFromPrincipalPortionMap = new ArrayList<>();
Money outstandingBalance = outstandingBalanceAsPerRest;
for (Map.Entry<LocalDate, Money> 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<LoanScheduleModelPeriod> periods, final Set<LoanCharge> 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<LoanCharge> separateTotalCompoundingPercentageCharges(final Set<LoanCharge> loanCharges) {
Set<LoanCharge> 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<LocalDate, Money> 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()));
}
}
@@ -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;
/**
* <p>
@@ -54,11 +56,26 @@ import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
* minus <b>interest due</b>.
* </p>
*/
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,
@@ -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<LocalDate, Money> principalVariation,
@@ -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;
}
}
@@ -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;
};
}
}
@@ -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;
}
@@ -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);
}
@@ -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<LoanTermVariationsData> variationsData) {
}
@@ -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<LocalDate, Money> principalVariation,
Map<LocalDate, Money> compoundingMap, LocalDate periodStartDate, LocalDate periodEndDate,
Collection<LoanTermVariationsData> 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()));
}
}
@@ -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;
}
}
@@ -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);
@@ -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);
@@ -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,
@@ -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);
}
}
@@ -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);
}
@@ -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<String, Object> 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;
}
}