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
@@ -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);
}