Fix loan repayment schedule with multidisburse

This commit is contained in:
Jose Alberto Hernandez
2022-09-22 10:58:23 +02:00
committed by Arnold Gálovics
parent c8a084cb97
commit 1521b9dcf6
17 changed files with 237 additions and 290 deletions
@@ -373,7 +373,6 @@ public final class CalendarUtils {
}
public static boolean isValidRedurringDate(final String recurringRule, final LocalDate seedDate, final LocalDate date) {
final Recur recur = CalendarUtils.getICalRecur(recurringRule);
if (recur == null) {
return false;
@@ -59,6 +59,7 @@ import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import javax.persistence.Version;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -142,6 +143,7 @@ import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDat
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.stereotype.Component;
@Slf4j
@Entity
@Component
@Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames = { "account_no" }, name = "loan_account_no_UNIQUE"),
@@ -5112,15 +5114,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName,
command.bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementPrincipalParameterName, locale));
Collection<LoanDisbursementDetails> loanDisburseDetails = this.getDisbursementDetails();
BigDecimal setPrincipalAmount = BigDecimal.ZERO;
for (LoanDisbursementDetails details : loanDisburseDetails) {
if (details.actualDisbursementDate() != null) {
setPrincipalAmount = setPrincipalAmount.add(details.principal());
}
}
this.loanRepaymentScheduleDetail.setPrincipal(getPrincipalAmountForRepaymentSchedule());
this.loanRepaymentScheduleDetail.setPrincipal(setPrincipalAmount);
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
} else {
@@ -5141,6 +5136,25 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return changedTransactionDetail;
}
public BigDecimal getPrincipalAmountForRepaymentSchedule() {
BigDecimal principalAmount = BigDecimal.ZERO;
if (isMultiDisburmentLoan() && isDisbursed()) {
Collection<LoanDisbursementDetails> loanDisburseDetails = this.getDisbursementDetails();
for (LoanDisbursementDetails details : loanDisburseDetails) {
if (details.actualDisbursementDate() != null) {
principalAmount = principalAmount.add(details.principal());
}
}
} else if (isApproved()) {
principalAmount = getApprovedPrincipal();
} else {
principalAmount = getPrincipal().getAmount();
}
return principalAmount;
}
public BigDecimal retriveLastEmiAmount() {
BigDecimal emiAmount = this.fixedEmiAmount;
LocalDate startDate = this.getDisbursementDate();
@@ -1259,8 +1259,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
*/
private Money getPrincipalToBeScheduled(final LoanApplicationTerms loanApplicationTerms) {
Money principalToBeScheduled;
if (loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getApprovedPrincipal().isGreaterThanZero()) {
principalToBeScheduled = loanApplicationTerms.getApprovedPrincipal();
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();
}
@@ -20,9 +20,11 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import net.fortuna.ical4j.model.Recur;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.service.HolidayUtil;
import org.apache.fineract.organisation.workingdays.data.AdjustedDateDetailsDTO;
import org.apache.fineract.organisation.workingdays.domain.RepaymentRescheduleType;
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
import org.apache.fineract.portfolio.calendar.data.CalendarHistoryDataWrapper;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
@@ -131,10 +133,14 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
private AdjustedDateDetailsDTO recursivelyCheckNonWorkingDaysAndHolidaysAndWorkingDaysExemptionToGenerateNextRepaymentPeriodDate(
final AdjustedDateDetailsDTO adjustedDateDetailsDTO, final LoanApplicationTerms loanApplicationTerms,
final HolidayDetailDTO holidayDetailDTO, final boolean isFirstRepayment) {
checkAndUpdateWorkingDayIfRepaymentDateIsNonWorkingDay(adjustedDateDetailsDTO, holidayDetailDTO, loanApplicationTerms,
isFirstRepayment);
final Recur recur = CalendarUtils.getICalRecur(holidayDetailDTO.getWorkingDays().getRecurrence());
final boolean isSevenDaysWeek = (recur.getDayList().size() == 7); // 7 Seven days in the week
// If Workings days are not seven day week
if (!isSevenDaysWeek) {
checkAndUpdateWorkingDayIfRepaymentDateIsNonWorkingDay(adjustedDateDetailsDTO, holidayDetailDTO, loanApplicationTerms,
isFirstRepayment);
}
// Check Holidays If applied
checkAndUpdateWorkingDayIfRepaymentDateIsHolidayDay(adjustedDateDetailsDTO, holidayDetailDTO, loanApplicationTerms,
isFirstRepayment);
@@ -148,6 +154,7 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
recursivelyCheckNonWorkingDaysAndHolidaysAndWorkingDaysExemptionToGenerateNextRepaymentPeriodDate(adjustedDateDetailsDTO,
loanApplicationTerms, holidayDetailDTO, isFirstRepayment);
}
return adjustedDateDetailsDTO;
}
@@ -192,10 +199,12 @@ public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
*/
private void checkAndUpdateWorkingDayIfRepaymentDateIsNonWorkingDay(final AdjustedDateDetailsDTO adjustedDateDetailsDTO,
final HolidayDetailDTO holidayDetailDTO, final LoanApplicationTerms loanApplicationTerms, final boolean isFirstRepayment) {
while (WorkingDaysUtil.isNonWorkingDay(holidayDetailDTO.getWorkingDays(), adjustedDateDetailsDTO.getChangedScheduleDate())) {
if (WorkingDaysUtil
.getRepaymentRescheduleType(holidayDetailDTO.getWorkingDays(), adjustedDateDetailsDTO.getChangedScheduleDate())
.isMoveToNextRepaymentDay()) {
final RepaymentRescheduleType repaymentRescheduleType = WorkingDaysUtil
.getRepaymentRescheduleType(holidayDetailDTO.getWorkingDays(), adjustedDateDetailsDTO.getChangedScheduleDate());
if (repaymentRescheduleType.isMoveToNextRepaymentDay()) {
while (WorkingDaysUtil.isNonWorkingDay(holidayDetailDTO.getWorkingDays(),
adjustedDateDetailsDTO.getNextRepaymentPeriodDueDate())
|| adjustedDateDetailsDTO.getChangedScheduleDate()
@@ -43,7 +43,6 @@ 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.data.LoanTermVariationsDataWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod;
@@ -252,32 +251,6 @@ public final class LoanApplicationTerms {
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, NthDayType nthDay, DayOfWeekType dayOfWeek,
final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate,
final LocalDate calculatedRepaymentsStartingFromDate, final Money inArrearsTolerance,
final LoanProductRelatedDetail loanProductRelatedDetail, final boolean multiDisburseLoan, final BigDecimal emiAmount,
final List<DisbursementData> disbursementDatas, final BigDecimal maxOutstandingBalance, final LocalDate interestChargedFromDate,
final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf,
final RecalculationFrequencyType recalculationFrequencyType, final CalendarInstance restCalendarInstance,
final InterestRecalculationCompoundingMethod compoundingMethod, final CalendarInstance compoundingCalendarInstance,
final RecalculationFrequencyType compoundingFrequencyType,
final LoanPreClosureInterestCalculationStrategy loanPreClosureInterestCalculationStrategy,
final LoanRescheduleStrategyMethod rescheduleStrategyMethod, BigDecimal approvedAmount, BigDecimal annualNominalInterestRate,
List<LoanTermVariationsData> loanTermVariations, final Integer numberOfDays, final boolean isSkipRepaymentOnFirstDayOfMonth,
final Calendar loanCalendar, final HolidayDetailDTO holidayDetailDTO, final boolean allowCompoundingOnEod) {
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
return assembleFrom(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, nthDay, dayOfWeek,
expectedDisbursementDate, repaymentsStartingFromDate, calculatedRepaymentsStartingFromDate, inArrearsTolerance,
loanProductRelatedDetail, multiDisburseLoan, emiAmount, disbursementDatas, maxOutstandingBalance, interestChargedFromDate,
principalThresholdForLastInstalment, installmentAmountInMultiplesOf, recalculationFrequencyType, restCalendarInstance,
compoundingMethod, compoundingCalendarInstance, compoundingFrequencyType, loanPreClosureInterestCalculationStrategy,
rescheduleStrategyMethod, loanCalendar, approvedAmount, annualNominalInterestRate, loanTermVariations,
calendarHistoryDataWrapper, numberOfDays, isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, false,
false, null, false);
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, NthDayType nthDay, DayOfWeekType dayOfWeek,
final LocalDate expectedDisbursementDate, final LocalDate repaymentsStartingFromDate,
@@ -337,93 +310,6 @@ public final class LoanApplicationTerms {
isPrincipalCompoundingDisabledForOverdueLoans);
}
public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applicationCurrency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final LocalDate expectedDisbursementDate,
final LocalDate repaymentsStartingFromDate, final LocalDate calculatedRepaymentsStartingFromDate,
final Money inArrearsTolerance, final LoanProductRelatedDetail loanProductRelatedDetail, final boolean multiDisburseLoan,
final BigDecimal emiAmount, final List<DisbursementData> disbursementDatas, final BigDecimal maxOutstandingBalance,
final LocalDate interestChargedFromDate, final LoanInterestRecalculationDetails interestRecalculationDetails,
final CalendarInstance restCalendarInstance, final RecalculationFrequencyType recalculationFrequencyType,
final CalendarInstance compoundingCalendarInstance, final RecalculationFrequencyType compoundingFrequencyType,
final BigDecimal principalThresholdForLastInstalment, final Integer installmentAmountInMultiplesOf,
final LoanPreClosureInterestCalculationStrategy loanPreClosureInterestCalculationStrategy, final Calendar loanCalendar,
BigDecimal approvedAmount, final BigDecimal annualNominalInterestRate, final List<LoanTermVariationsData> loanTermVariations,
Integer numberOfDays, boolean isSkipRepaymentOnFirstDayOfMonth, final HolidayDetailDTO holidayDetailDTO,
final boolean allowCompoundingOnEod) {
final Integer numberOfRepayments = loanProductRelatedDetail.getNumberOfRepayments();
final Integer repaymentEvery = loanProductRelatedDetail.getRepayEvery();
final PeriodFrequencyType repaymentPeriodFrequencyType = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
final AmortizationMethod amortizationMethod = loanProductRelatedDetail.getAmortizationMethod();
final InterestMethod interestMethod = loanProductRelatedDetail.getInterestMethod();
final BigDecimal interestRatePerPeriod = loanProductRelatedDetail.getNominalInterestRatePerPeriod();
final PeriodFrequencyType interestRatePeriodFrequencyType = loanProductRelatedDetail.getInterestPeriodFrequencyType();
final InterestCalculationPeriodMethod interestCalculationPeriodMethod = loanProductRelatedDetail
.getInterestCalculationPeriodMethod();
final boolean allowPartialPeriodInterestCalcualtion = loanProductRelatedDetail.isAllowPartialPeriodInterestCalcualtion();
final Money principalMoney = loanProductRelatedDetail.getPrincipal();
//
final Integer graceOnPrincipalPayment = loanProductRelatedDetail.graceOnPrincipalPayment();
final Integer recurringMoratoriumOnPrincipalPeriods = loanProductRelatedDetail.recurringMoratoriumOnPrincipalPeriods();
final Integer graceOnInterestPayment = loanProductRelatedDetail.graceOnInterestPayment();
final Integer graceOnInterestCharged = loanProductRelatedDetail.graceOnInterestCharged();
// Interest recalculation settings
final DaysInMonthType daysInMonthType = loanProductRelatedDetail.fetchDaysInMonthType();
final DaysInYearType daysInYearType = loanProductRelatedDetail.fetchDaysInYearType();
final boolean isInterestRecalculationEnabled = loanProductRelatedDetail.isInterestRecalculationEnabled();
LoanRescheduleStrategyMethod rescheduleStrategyMethod = null;
InterestRecalculationCompoundingMethod interestRecalculationCompoundingMethod = null;
if (isInterestRecalculationEnabled) {
rescheduleStrategyMethod = interestRecalculationDetails.getRescheduleStrategyMethod();
interestRecalculationCompoundingMethod = interestRecalculationDetails.getInterestRecalculationCompoundingMethod();
}
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
final boolean isInterestChargedFromDateSameAsDisbursalDateEnabled = false;
final boolean isEqualAmortization = loanProductRelatedDetail.isEqualAmortization();
return new LoanApplicationTerms(applicationCurrency, loanTermFrequency, loanTermPeriodFrequencyType, numberOfRepayments,
repaymentEvery, repaymentPeriodFrequencyType, null, null, amortizationMethod, interestMethod, interestRatePerPeriod,
interestRatePeriodFrequencyType, annualNominalInterestRate, interestCalculationPeriodMethod,
allowPartialPeriodInterestCalcualtion, principalMoney, expectedDisbursementDate, repaymentsStartingFromDate,
calculatedRepaymentsStartingFromDate, graceOnPrincipalPayment, recurringMoratoriumOnPrincipalPeriods,
graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, inArrearsTolerance, multiDisburseLoan, emiAmount,
disbursementDatas, maxOutstandingBalance, loanProductRelatedDetail.getGraceOnDueDate(), daysInMonthType, daysInYearType,
isInterestRecalculationEnabled, rescheduleStrategyMethod, interestRecalculationCompoundingMethod, restCalendarInstance,
recalculationFrequencyType, compoundingCalendarInstance, compoundingFrequencyType, principalThresholdForLastInstalment,
installmentAmountInMultiplesOf, loanPreClosureInterestCalculationStrategy, loanCalendar, approvedAmount, loanTermVariations,
calendarHistoryDataWrapper, isInterestChargedFromDateSameAsDisbursalDateEnabled, numberOfDays,
isSkipRepaymentOnFirstDayOfMonth, holidayDetailDTO, allowCompoundingOnEod, isEqualAmortization, false, false, null, false);
}
public static LoanApplicationTerms assembleFrom(final LoanApplicationTerms applicationTerms,
final List<LoanTermVariationsData> loanTermVariations) {
return new LoanApplicationTerms(applicationTerms.currency, applicationTerms.loanTermFrequency,
applicationTerms.loanTermPeriodFrequencyType, applicationTerms.numberOfRepayments, applicationTerms.repaymentEvery,
applicationTerms.repaymentPeriodFrequencyType, applicationTerms.nthDay, applicationTerms.weekDayType,
applicationTerms.amortizationMethod, applicationTerms.interestMethod, applicationTerms.interestRatePerPeriod,
applicationTerms.interestRatePeriodFrequencyType, applicationTerms.annualNominalInterestRate,
applicationTerms.interestCalculationPeriodMethod, applicationTerms.allowPartialPeriodInterestCalcualtion,
applicationTerms.principal, applicationTerms.expectedDisbursementDate, applicationTerms.repaymentsStartingFromDate,
applicationTerms.calculatedRepaymentsStartingFromDate, applicationTerms.principalGrace,
applicationTerms.recurringMoratoriumOnPrincipalPeriods, applicationTerms.interestPaymentGrace,
applicationTerms.interestChargingGrace, applicationTerms.interestChargedFromDate, applicationTerms.inArrearsTolerance,
applicationTerms.multiDisburseLoan, applicationTerms.actualFixedEmiAmount, applicationTerms.disbursementDatas,
applicationTerms.maxOutstandingBalance, applicationTerms.graceOnArrearsAgeing, applicationTerms.daysInMonthType,
applicationTerms.daysInYearType, applicationTerms.interestRecalculationEnabled, applicationTerms.rescheduleStrategyMethod,
applicationTerms.interestRecalculationCompoundingMethod, applicationTerms.restCalendarInstance,
applicationTerms.recalculationFrequencyType, applicationTerms.compoundingCalendarInstance,
applicationTerms.compoundingFrequencyType, applicationTerms.principalThresholdForLastInstalment,
applicationTerms.installmentAmountInMultiplesOf, applicationTerms.preClosureInterestCalculationStrategy,
applicationTerms.loanCalendar, applicationTerms.approvedPrincipal.getAmount(), loanTermVariations,
applicationTerms.calendarHistoryDataWrapper, applicationTerms.isInterestChargedFromDateSameAsDisbursalDateEnabled,
applicationTerms.numberOfDays, applicationTerms.isSkipRepaymentOnFirstDayOfMonth, applicationTerms.holidayDetailDTO,
applicationTerms.allowCompoundingOnEod, applicationTerms.isEqualAmortization,
applicationTerms.isFirstRepaymentDateAllowedOnHoliday, applicationTerms.isInterestToBeRecoveredFirstWhenGreaterThanEMI,
applicationTerms.fixedPrincipalPercentagePerInstallment, applicationTerms.isPrincipalCompoundingDisabledForOverdueLoans);
}
private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency,
final PeriodFrequencyType loanTermPeriodFrequencyType, final Integer numberOfRepayments, final Integer repaymentEvery,
final PeriodFrequencyType repaymentPeriodFrequencyType, final Integer nthDay, final DayOfWeekType weekDayType,
@@ -1702,6 +1588,18 @@ public final class LoanApplicationTerms {
return disbursedAmount;
}
public Money getTotalMultiDisbursedAmount() {
Money disbursedAmount = Money.zero(getCurrency());
if (isMultiDisburseLoan()) {
for (DisbursementData disbursement : getDisbursementDatas()) {
disbursedAmount = disbursedAmount.plus(disbursement.getPrincipal());
}
} else {
disbursedAmount = getPrincipal();
}
return disbursedAmount;
}
public void updatePeriodNumberApplicableForPrincipalOrInterestGrace(final Integer periodsApplicationForGrace) {
int applicablePeriodNumber = periodsApplicationForGrace;
int graceOnPrincipal = defaultToZeroIfNull(this.principalGrace);
@@ -2996,20 +2996,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ChangedTransactionDetail changedTransactionDetail = null;
if (command.entityId() != null) {
changedTransactionDetail = loan.updateDisbursementDateAndAmountForTranche(loanDisbursementDetails, command, changes,
scheduleGeneratorDTO);
} else {
// BigDecimal setAmount = loan.getApprovedPrincipal();
Collection<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails();
BigDecimal setAmount = BigDecimal.ZERO;
for (LoanDisbursementDetails details : loanDisburseDetails) {
if (details.actualDisbursementDate() != null) {
setAmount = setAmount.add(details.principal());
}
}
loan.repaymentScheduleDetail().setPrincipal(setAmount);
loan.repaymentScheduleDetail().setPrincipal(loan.getPrincipalAmountForRepaymentSchedule());
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);