FINERACT-1981: EMI Calculation, Daily, Weekly, Monthly support plug to Repayment Schedule

This commit is contained in:
Janos Meszaros
2024-07-03 20:41:45 +02:00
committed by Adam Saghy
parent 112787cf07
commit cfdfa6cd57
8 changed files with 31 additions and 26 deletions
@@ -275,7 +275,7 @@ public final class LoanSchedulePeriodData {
this.dueDate = dueDate; this.dueDate = dueDate;
this.obligationsMetOnDate = null; this.obligationsMetOnDate = null;
this.complete = null; this.complete = null;
this.daysInPeriod = 1; this.daysInPeriod = 1; // TODO: check this may should be 0 or somewhere else should be 1?
this.principalDisbursed = null; this.principalDisbursed = null;
this.principalOriginalDue = principalDue; this.principalOriginalDue = principalDue;
this.principalDue = principalOriginalDue; this.principalDue = principalOriginalDue;
@@ -27,7 +27,7 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
public interface ScheduledDateGenerator { public interface ScheduledDateGenerator {
List<PreGeneratedLoanSchedulePeriod> generateRepaymentPeriods(LoanApplicationTerms loanApplicationTerms, List<PreGeneratedLoanSchedulePeriod> generateRepaymentPeriods(LocalDate scheduledDueDate, LoanApplicationTerms loanApplicationTerms,
HolidayDetailDTO holidayDetailDTO); HolidayDetailDTO holidayDetailDTO);
LocalDate getLastRepaymentDate(LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO); LocalDate getLastRepaymentDate(LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO);
@@ -85,11 +85,7 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
List<LoanScheduleModelPeriod> periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, scheduleParams, List<LoanScheduleModelPeriod> periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms, scheduleParams,
chargesDueAtTimeOfDisbursement); chargesDueAtTimeOfDisbursement);
List<PreGeneratedLoanSchedulePeriod> expectedRepaymentPeriods = getScheduledDateGenerator() EMICalculationResult emiCalculationResult = null;
.generateRepaymentPeriods(loanApplicationTerms, holidayDetailDTO);
EMICalculationResult emiCalculationResult = getEMICalculator().calculateEMIValueAndRateFactors(scheduleParams,
loanApplicationTerms.toLoanProductRelatedDetail(), expectedRepaymentPeriods, mc);
boolean isFirstRepayment = true; boolean isFirstRepayment = true;
// charges which depends on total loan interest will be added to this // charges which depends on total loan interest will be added to this
@@ -105,7 +101,6 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
AdjustedDateDetailsDTO adjustedDateDetailsDTO = getScheduledDateGenerator() AdjustedDateDetailsDTO adjustedDateDetailsDTO = getScheduledDateGenerator()
.adjustRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, holidayDetailDTO); .adjustRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, holidayDetailDTO);
scheduleParams.setActualRepaymentDate(adjustedDateDetailsDTO.getChangedActualRepaymentDate()); scheduleParams.setActualRepaymentDate(adjustedDateDetailsDTO.getChangedActualRepaymentDate());
isFirstRepayment = false;
LocalDate scheduledDueDate = adjustedDateDetailsDTO.getChangedScheduleDate(); LocalDate scheduledDueDate = adjustedDateDetailsDTO.getChangedScheduleDate();
// Loan Schedule Exceptions that need to be applied for Loan Account // Loan Schedule Exceptions that need to be applied for Loan Account
@@ -117,8 +112,16 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
.addLoanTermInDays(Math.toIntExact(ChronoUnit.DAYS.between(scheduleParams.getPeriodStartDate(), scheduledDueDate))); .addLoanTermInDays(Math.toIntExact(ChronoUnit.DAYS.between(scheduleParams.getPeriodStartDate(), scheduledDueDate)));
ScheduleCurrentPeriodParams currentPeriodParams = new ScheduleCurrentPeriodParams(currency, BigDecimal.ZERO); ScheduleCurrentPeriodParams currentPeriodParams = new ScheduleCurrentPeriodParams(currency, BigDecimal.ZERO);
final boolean hasAnyProcessedDisbursement = processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement,
processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods, scheduledDueDate); scheduleParams, periods, scheduledDueDate);
if (isFirstRepayment || hasAnyProcessedDisbursement) {
final LocalDate startDate = isFirstRepayment ? loanApplicationTerms.getRepaymentStartDate()
: scheduleParams.getPeriodStartDate();
List<PreGeneratedLoanSchedulePeriod> expectedRepaymentPeriods = getScheduledDateGenerator()
.generateRepaymentPeriods(startDate, loanApplicationTerms, holidayDetailDTO);
emiCalculationResult = getEMICalculator().calculateEMIValueAndRateFactors(scheduleParams,
loanApplicationTerms.toLoanProductRelatedDetail(), expectedRepaymentPeriods, mc);
}
// 5 determine principal,interest of repayment period // 5 determine principal,interest of repayment period
PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(loanApplicationTerms, PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod(loanApplicationTerms,
@@ -189,6 +192,7 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
// } // }
thereIsDisbursementBeforeOrOnLoanEndDate = scheduleParams.getDisburseDetailMap().entrySet().stream() thereIsDisbursementBeforeOrOnLoanEndDate = scheduleParams.getDisburseDetailMap().entrySet().stream()
.anyMatch(d -> !d.getKey().isAfter(loanApplicationTerms.getLoanEndDate())); .anyMatch(d -> !d.getKey().isAfter(loanApplicationTerms.getLoanEndDate()));
isFirstRepayment = false;
} }
// If the disbursement happened after maturity date // If the disbursement happened after maturity date
@@ -327,9 +331,10 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
/** /**
* Method add extra disbursement periods (if applicable) and update the schedule params * Method add extra disbursement periods (if applicable) and update the schedule params
*/ */
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement, private boolean processDisbursements(final LoanApplicationTerms loanApplicationTerms, final BigDecimal chargesDueAtTimeOfDisbursement,
LoanScheduleParams scheduleParams, final Collection<LoanScheduleModelPeriod> periods, final LocalDate scheduledDueDate) { LoanScheduleParams scheduleParams, final Collection<LoanScheduleModelPeriod> periods, final LocalDate scheduledDueDate) {
Iterator<Map.Entry<LocalDate, Money>> iter = scheduleParams.getDisburseDetailMap().entrySet().iterator(); Iterator<Map.Entry<LocalDate, Money>> iter = scheduleParams.getDisburseDetailMap().entrySet().iterator();
boolean hasProcessedDisbursement = false;
while (iter.hasNext()) { while (iter.hasNext()) {
Map.Entry<LocalDate, Money> disburseDetail = iter.next(); Map.Entry<LocalDate, Money> disburseDetail = iter.next();
if ((disburseDetail.getKey().isEqual(scheduleParams.getPeriodStartDate()) if ((disburseDetail.getKey().isEqual(scheduleParams.getPeriodStartDate())
@@ -373,8 +378,10 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
} }
} }
iter.remove(); iter.remove();
hasProcessedDisbursement = true;
} }
} }
return hasProcessedDisbursement;
} }
private void applyChargesForCurrentPeriod(final Set<LoanCharge> loanCharges, final MonetaryCurrency currency, private void applyChargesForCurrentPeriod(final Set<LoanCharge> loanCharges, final MonetaryCurrency currency,
@@ -54,8 +54,11 @@ public class ProgressiveLoanScheduleGenerator extends AbstractProgressiveLoanSch
public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final LoanApplicationTerms loanApplicationTerms, public PrincipalInterest calculatePrincipalInterestComponentsForPeriod(final LoanApplicationTerms loanApplicationTerms,
final LoanScheduleParams loanScheduleParams, final EMICalculationResult emiCalculationResult, final MathContext mc) { final LoanScheduleParams loanScheduleParams, final EMICalculationResult emiCalculationResult, final MathContext mc) {
final Money equalMonthlyInstallmentValue = Money.of(loanApplicationTerms.getCurrency(), final Money equalMonthlyInstallmentValue = loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null
emiCalculationResult.getEqualMonthlyInstallmentValue()); ? Money.of(loanApplicationTerms.getCurrency(),
Money.roundToMultiplesOf(emiCalculationResult.getEqualMonthlyInstallmentValue(),
loanApplicationTerms.getInstallmentAmountInMultiplesOf()))
: Money.of(loanApplicationTerms.getCurrency(), emiCalculationResult.getEqualMonthlyInstallmentValue());
final BigDecimal rateFactorMinus1 = emiCalculationResult.getNextRepaymentPeriodRateFactorMinus1(); final BigDecimal rateFactorMinus1 = emiCalculationResult.getNextRepaymentPeriodRateFactorMinus1();
final Money calculatedInterest = loanScheduleParams.getOutstandingBalance().multipliedBy(rateFactorMinus1); final Money calculatedInterest = loanScheduleParams.getOutstandingBalance().multipliedBy(rateFactorMinus1);
final Money calculatedPrincipal = equalMonthlyInstallmentValue.minus(calculatedInterest); final Money calculatedPrincipal = equalMonthlyInstallmentValue.minus(calculatedInterest);
@@ -58,12 +58,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
public EMICalculationResult calculateEMIValueAndRateFactors(final LoanScheduleParams scheduleParams, public EMICalculationResult calculateEMIValueAndRateFactors(final LoanScheduleParams scheduleParams,
final LoanProductRelatedDetail loanProductRelatedDetail, final List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods, final LoanProductRelatedDetail loanProductRelatedDetail, final List<? extends LoanScheduleModelPeriod> expectedRepaymentPeriods,
final MathContext mc) { final MathContext mc) {
// TODO: decide which outstanding balance method needed in the future
final BigDecimal nominalInterestRatePerPeriod = calcNominalInterestRatePerPeriod( final BigDecimal nominalInterestRatePerPeriod = calcNominalInterestRatePerPeriod(
loanProductRelatedDetail.getNominalInterestRatePerPeriod(), mc); loanProductRelatedDetail.getNominalInterestRatePerPeriod(), mc);
final BigDecimal outstandingBalance = scheduleParams.getOutstandingBalanceAsPerRest().getAmount().compareTo(BigDecimal.ZERO) > 0 final BigDecimal outstandingBalance = scheduleParams.getOutstandingBalanceAsPerRest().getAmount();
? scheduleParams.getOutstandingBalanceAsPerRest().getAmount()
: loanProductRelatedDetail.getPrincipal().getAmount();
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType()); final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType()); final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType(); final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
@@ -66,7 +66,7 @@ class ProgressiveEMICalculatorTest {
@BeforeAll @BeforeAll
public static void init() { public static void init() {
periods = new ArrayList<>(); periods = new ArrayList<>();
LocalDate startDate = LocalDate.of(2024, 01, 1); LocalDate startDate = LocalDate.of(2024, 1, 1);
periods.add(createPeriod(1, startDate, startDate.plusMonths(1))); periods.add(createPeriod(1, startDate, startDate.plusMonths(1)));
periods.add(createPeriod(2, startDate.plusMonths(1), startDate.plusMonths(2))); periods.add(createPeriod(2, startDate.plusMonths(1), startDate.plusMonths(2)));
periods.add(createPeriod(3, startDate.plusMonths(2), startDate.plusMonths(3))); periods.add(createPeriod(3, startDate.plusMonths(2), startDate.plusMonths(3)));
@@ -495,11 +495,9 @@ class ProgressiveEMICalculatorTest {
final MathContext mc = MoneyHelper.getMathContext(); final MathContext mc = MoneyHelper.getMathContext();
final BigDecimal interestRate = BigDecimal.valueOf(0); final BigDecimal interestRate = BigDecimal.valueOf(0);
final Money principal = Money.of(monetaryCurrency, BigDecimal.valueOf(1000)); final Money outstandingBalance = Money.of(monetaryCurrency, BigDecimal.valueOf(1000));
final Money outstandingBalance = Money.of(monetaryCurrency, BigDecimal.ZERO);
Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance); Mockito.when(scheduleParams.getOutstandingBalanceAsPerRest()).thenReturn(outstandingBalance);
Mockito.when(loanProductRelatedDetail.getPrincipal()).thenReturn(principal);
Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
@@ -43,12 +43,11 @@ import org.springframework.stereotype.Component;
public class DefaultScheduledDateGenerator implements ScheduledDateGenerator { public class DefaultScheduledDateGenerator implements ScheduledDateGenerator {
@Override @Override
public List<PreGeneratedLoanSchedulePeriod> generateRepaymentPeriods(final LoanApplicationTerms loanApplicationTerms, public List<PreGeneratedLoanSchedulePeriod> generateRepaymentPeriods(final LocalDate scheduledDueDate,
final HolidayDetailDTO holidayDetailDTO) { final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO) {
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments(); final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
final ArrayList<PreGeneratedLoanSchedulePeriod> repaymentPeriods = new ArrayList<>(numberOfRepayments); final ArrayList<PreGeneratedLoanSchedulePeriod> repaymentPeriods = new ArrayList<>(numberOfRepayments);
// TODO: check periodStartDate LocalDate lastRepaymentDate = scheduledDueDate;
LocalDate lastRepaymentDate = loanApplicationTerms.getRepaymentStartDate();
LocalDate nextRepaymentDate; LocalDate nextRepaymentDate;
boolean isFirstRepayment = true; boolean isFirstRepayment = true;
for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) { for (int repaymentPeriod = 1; repaymentPeriod <= numberOfRepayments; repaymentPeriod++) {
@@ -98,7 +98,8 @@ public class DefaultScheduledDateGeneratorTest {
submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null); submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null);
// when // when
List<PreGeneratedLoanSchedulePeriod> result = underTest.generateRepaymentPeriods(loanApplicationTerms, holidayDetailDTO); List<PreGeneratedLoanSchedulePeriod> result = underTest.generateRepaymentPeriods(expectedDisbursementDate, loanApplicationTerms,
holidayDetailDTO);
// then // then
assertThat(result.size()).isEqualTo(4); assertThat(result.size()).isEqualTo(4);