FINERACT-1981: Generate loan schedule as dto

This commit is contained in:
Adam Saghy
2024-10-25 14:35:29 +02:00
parent b23fe71864
commit e60ce9a401
16 changed files with 507 additions and 236 deletions
@@ -828,7 +828,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount);
if (amortizableAmount.isGreaterThanZero()) {
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode());
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
candidateRepaymentInstallments.forEach(i -> {
@@ -1194,7 +1194,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
case IN_ADVANCE -> {
int numberOfInstallments = inAdvanceInstallments.size();
if (numberOfInstallments > 0) {
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
@@ -1262,7 +1262,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
int numberOfInstallments = currentInstallments.size();
refundedPortion = zero;
if (numberOfInstallments > 0) {
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
currentInstallment = internalCurrentInstallment;
@@ -1467,7 +1467,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (numberOfInstallments > 0) {
// This will be the same amount as transactionAmountUnprocessed in case of the future
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
// Adjustment might be needed due to the divide operation and the rounding mode
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
@@ -1645,7 +1645,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (numberOfInstallments > 0) {
// This will be the same amount as transactionAmountUnprocessed in case of the future
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getMathContext());
// Adjustment might be needed due to the divide operation and the rounding mode
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
@@ -1723,7 +1723,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Money adjustCalculatedPrincipal = Money.zero(currency);
if (outstandingPrincipalBalance.get().isGreaterThanZero()) {
calculatedPrincipal = outstandingPrincipalBalance.get()
.dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getRoundingMode());
.dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getMathContext());
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
if (installmentAmountInMultiplesOf != null) {
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
@@ -0,0 +1,90 @@
/**
* 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.data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelDisbursementPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
@Data
public class LoanSchedulePlan {
private final List<LoanSchedulePlanPeriod> periods;
private final CurrencyData currency;
private final int loanTermInDays;
private final BigDecimal totalDisbursedAmount;
private final BigDecimal totalPrincipalAmount;
private final BigDecimal totalInterestAmount;
private final BigDecimal totalFeeAmount;
private final BigDecimal totalPenaltyAmount;
private final BigDecimal totalRepaymentAmount;
private final BigDecimal totalOutstandingAmount;
public static LoanSchedulePlan from(LoanScheduleModel model) {
List<LoanSchedulePlanPeriod> periods = new ArrayList<>();
model.getPeriods().forEach(periodModel -> {
LoanSchedulePlanPeriod periodPlan = null;
if (periodModel instanceof LoanScheduleModelDisbursementPeriod disbursementPeriod) {
periodPlan = new LoanSchedulePlanDisbursementPeriod(disbursementPeriod.getDisbursementDate(), //
disbursementPeriod.getDisbursementDate(), //
disbursementPeriod.getPrincipalDisbursed().getAmount(), //
disbursementPeriod.getPrincipalDisbursed().getAmount());//
} else if (periodModel instanceof LoanScheduleModelDownPaymentPeriod downPaymentPeriod) {
periodPlan = new LoanSchedulePlanDownPaymentPeriod(downPaymentPeriod.getPeriodNumber(), //
downPaymentPeriod.getPeriodDate(), //
downPaymentPeriod.getPeriodDate(), //
downPaymentPeriod.getPrincipalDue().getAmount(), //
downPaymentPeriod.getPrincipalDue().getAmount(), //
downPaymentPeriod.getOutstandingLoanBalance().getAmount());//
} else if (periodModel instanceof LoanScheduleModelRepaymentPeriod repaymentPeriod) {
periodPlan = new LoanSchedulePlanRepaymentPeriod(repaymentPeriod.getPeriodNumber(), //
repaymentPeriod.getFromDate(), //
repaymentPeriod.getDueDate(), //
repaymentPeriod.getPrincipalDue().getAmount(), //
repaymentPeriod.getInterestDue().getAmount(), //
repaymentPeriod.getFeeChargesDue().getAmount(), //
repaymentPeriod.getPenaltyChargesDue().getAmount(), //
repaymentPeriod.getTotalDue().getAmount(), //
repaymentPeriod.getOutstandingLoanBalance().getAmount());//
}
if (periodPlan != null) {
periods.add(periodPlan);
}
});
return new LoanSchedulePlan(periods, //
model.getApplicationCurrency().toData(), //
model.getLoanTermInDays(), //
model.getTotalPrincipalDisbursed().getAmount(), //
model.getTotalPrincipalExpected(), //
model.getTotalInterestCharged(), //
model.getTotalFeeChargesCharged(), //
model.getTotalPenaltyChargesCharged(), //
model.getTotalRepaymentExpected(), //
model.getTotalOutstanding()//
);
}
}
@@ -0,0 +1,47 @@
/**
* 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.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Data;
@Data
public final class LoanSchedulePlanDisbursementPeriod implements LoanSchedulePlanPeriod {
private final LocalDate periodFromDate;
private final LocalDate periodDueDate;
private final BigDecimal principalAmount;
private final BigDecimal outstandingLoanBalance;
@Override
public Integer periodNumber() {
return null;
}
@Override
public LocalDate periodFromDate() {
return periodFromDate;
}
@Override
public LocalDate periodDueDate() {
return periodDueDate;
}
}
@@ -0,0 +1,49 @@
/**
* 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.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Data;
@Data
public final class LoanSchedulePlanDownPaymentPeriod implements LoanSchedulePlanPeriod {
private final int periodNumber;
private final LocalDate periodFromDate;
private final LocalDate periodDueDate;
private final BigDecimal principalAmount;
private final BigDecimal totalDueAmount;
private final BigDecimal outstandingLoanBalance;
@Override
public Integer periodNumber() {
return periodNumber;
}
@Override
public LocalDate periodFromDate() {
return periodFromDate;
}
@Override
public LocalDate periodDueDate() {
return periodDueDate;
}
}
@@ -0,0 +1,30 @@
/**
* 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.data;
import java.time.LocalDate;
public interface LoanSchedulePlanPeriod {
Integer periodNumber();
LocalDate periodFromDate();
LocalDate periodDueDate();
}
@@ -0,0 +1,52 @@
/**
* 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.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Data;
@Data
public final class LoanSchedulePlanRepaymentPeriod implements LoanSchedulePlanPeriod {
private final int periodNumber;
private final LocalDate periodFromDate;
private final LocalDate periodDueDate;
private final BigDecimal principalAmount;
private final BigDecimal interestAmount;
private final BigDecimal feeAmount;
private final BigDecimal penaltyAmount;
private final BigDecimal totalDueAmount;
private final BigDecimal outstandingLoanBalance;
@Override
public Integer periodNumber() {
return periodNumber;
}
@Override
public LocalDate periodFromDate() {
return periodFromDate;
}
@Override
public LocalDate periodDueDate() {
return periodDueDate;
}
}
@@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
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.data.LoanSchedulePlan;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
@@ -165,11 +166,10 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
}
public LoanScheduleModel generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) {
LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc);
return generate(mc, loanApplicationTerms, null, null);
return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null));
}
private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) {
@@ -221,22 +221,22 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
Money downPaymentAmount = Money.zero(loanApplicationTerms.getCurrency(), mc);
if (loanApplicationTerms.isDownPaymentEnabled()) {
downPaymentAmount = Money.of(loanApplicationTerms.getCurrency(), MathUtil.percentageOf(disbursedAmount.getAmount(),
loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19));
loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), mc), mc);
if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) {
downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount,
loanApplicationTerms.getInstallmentAmountInMultiplesOf());
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
}
LoanScheduleModelDownPaymentPeriod downPaymentPeriod = LoanScheduleModelDownPaymentPeriod.downPayment(
scheduleParams.getInstalmentNumber(), disbursementDate, downPaymentAmount,
outstandingBalance.plus(disbursedAmount).minus(downPaymentAmount));
outstandingBalance.plus(disbursedAmount, mc).minus(downPaymentAmount, mc));
periods.add(downPaymentPeriod);
scheduleParams.addTotalRepaymentExpected(downPaymentAmount);
scheduleParams.incrementInstalmentNumber();
}
final Money disbursementRemainingBalance = disbursedAmount.minus(downPaymentAmount);
final Money disbursementRemainingBalance = disbursedAmount.minus(downPaymentAmount, mc);
scheduleParams.addPrincipalToBeScheduled(disbursementRemainingBalance);
emiCalculator.addDisbursement(interestScheduleModel, disbursementDate, disbursementRemainingBalance);
}
@@ -237,7 +237,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc));
if (shouldBeAdjusted) {
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc.getRoundingMode(), mc);
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc);
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel,
originalEmi.plus(adjustment, mc));
if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) {
@@ -19,79 +19,135 @@
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDisbursementPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDownPaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.ProgressiveEMICalculator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class LoanScheduleGeneratorTest {
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$");
private static final BigDecimal DISBURSEMENT_AMOUNT = BigDecimal.valueOf(192.22);
private static final BigDecimal DISBURSEMENT_AMOUNT_100 = BigDecimal.valueOf(100);
private static final BigDecimal NOMINAL_INTEREST_RATE = BigDecimal.valueOf(9.99);
private static final int NUMBER_OF_REPAYMENTS = 6;
private static final int REPAYMENT_FREQUENCY = 1;
private static final String REPAYMENT_FREQUENCY_TYPE = "MONTHS";
private static final LocalDate DISBURSEMENT_DATE = LocalDate.of(2024, 1, 15);
private static final MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
private static final BigDecimal DOWN_PAYMENT_PORTION = BigDecimal.valueOf(25);
@Test
void testGenerateLoanSchedule() {
List<LoanScheduleModelPeriod> periods = getLoanScheduleModelPeriods();
assertEquals(7, periods.size(), "Expected 7 periods including the downpayment period.");
LoanScheduleModelDisbursementPeriod disbursementPeriod = (LoanScheduleModelDisbursementPeriod) periods.get(0);
assertNotNull(disbursementPeriod);
checkPeriod(periods.get(1), 1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), BigDecimal.valueOf(31.97),
BigDecimal.valueOf(0.88), BigDecimal.valueOf(32.85), BigDecimal.valueOf(160.25));
checkPeriod(periods.get(2), 2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), BigDecimal.valueOf(31.52),
BigDecimal.valueOf(1.33), BigDecimal.valueOf(32.85), BigDecimal.valueOf(128.73));
checkPeriod(periods.get(3), 3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), BigDecimal.valueOf(31.78),
BigDecimal.valueOf(1.07), BigDecimal.valueOf(32.85), BigDecimal.valueOf(96.95));
checkPeriod(periods.get(4), 4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), BigDecimal.valueOf(32.04),
BigDecimal.valueOf(0.81), BigDecimal.valueOf(32.85), BigDecimal.valueOf(64.91));
checkPeriod(periods.get(5), 5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), BigDecimal.valueOf(32.31),
BigDecimal.valueOf(0.54), BigDecimal.valueOf(32.85), BigDecimal.valueOf(32.60));
checkPeriod(periods.get(6), 6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), BigDecimal.valueOf(32.60),
BigDecimal.valueOf(0.27), BigDecimal.valueOf(32.87), BigDecimal.ZERO);
}
private static List<LoanScheduleModelPeriod> getLoanScheduleModelPeriods() {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), APPLICATION_CURRENCY,
DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null);
NOMINAL_INTEREST_RATE, false, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null);
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator);
LoanScheduleModel loanSchedule = generator.generate(mc, modelData);
return loanSchedule.getPeriods();
LoanSchedulePlan loanSchedule = generator.generate(mc, modelData);
assertEquals(7, loanSchedule.getPeriods().size(), "Expected 7 periods without the downpayment period.");
checkDisbursementPeriod((LoanSchedulePlanDisbursementPeriod) loanSchedule.getPeriods().get(0), LocalDate.of(2024, 1, 15),
DISBURSEMENT_AMOUNT, DISBURSEMENT_AMOUNT);
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(1), 1, LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 2, 1), BigDecimal.valueOf(31.97), BigDecimal.valueOf(0.88), BigDecimal.valueOf(32.85),
BigDecimal.valueOf(160.25));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(2), 2, LocalDate.of(2024, 2, 1),
LocalDate.of(2024, 3, 1), BigDecimal.valueOf(31.52), BigDecimal.valueOf(1.33), BigDecimal.valueOf(32.85),
BigDecimal.valueOf(128.73));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(3), 3, LocalDate.of(2024, 3, 1),
LocalDate.of(2024, 4, 1), BigDecimal.valueOf(31.78), BigDecimal.valueOf(1.07), BigDecimal.valueOf(32.85),
BigDecimal.valueOf(96.95));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(4), 4, LocalDate.of(2024, 4, 1),
LocalDate.of(2024, 5, 1), BigDecimal.valueOf(32.04), BigDecimal.valueOf(0.81), BigDecimal.valueOf(32.85),
BigDecimal.valueOf(64.91));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(5), 5, LocalDate.of(2024, 5, 1),
LocalDate.of(2024, 6, 1), BigDecimal.valueOf(32.31), BigDecimal.valueOf(0.54), BigDecimal.valueOf(32.85),
BigDecimal.valueOf(32.60));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(6), 6, LocalDate.of(2024, 6, 1),
LocalDate.of(2024, 7, 1), BigDecimal.valueOf(32.60), BigDecimal.valueOf(0.27), BigDecimal.valueOf(32.87), BigDecimal.ZERO);
}
private void checkPeriod(LoanScheduleModelPeriod period, int expectedPeriodNumber, LocalDate expectedFromDate,
@Test
void testGenerateLoanScheduleWithDownPayment() {
LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), APPLICATION_CURRENCY,
DISBURSEMENT_AMOUNT_100, LocalDate.of(2024, 1, 1), NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE,
NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, DOWN_PAYMENT_PORTION, null, null);
ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator);
LoanSchedulePlan loanSchedule = generator.generate(mc, modelData);
assertEquals(8, loanSchedule.getPeriods().size(), "Expected 8 periods with the downpayment period.");
checkDisbursementPeriod((LoanSchedulePlanDisbursementPeriod) loanSchedule.getPeriods().get(0), LocalDate.of(2024, 1, 1),
DISBURSEMENT_AMOUNT_100, DISBURSEMENT_AMOUNT_100);
checkDownPaymentPeriod((LoanSchedulePlanDownPaymentPeriod) loanSchedule.getPeriods().get(1), 1, LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 1, 1), BigDecimal.valueOf(25.0), BigDecimal.valueOf(25.0), BigDecimal.valueOf(75.0));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(2), 2, LocalDate.of(2024, 1, 1),
LocalDate.of(2024, 2, 1), BigDecimal.valueOf(12.25), BigDecimal.valueOf(0.62), BigDecimal.valueOf(12.87),
BigDecimal.valueOf(62.75));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(3), 3, LocalDate.of(2024, 2, 1),
LocalDate.of(2024, 3, 1), BigDecimal.valueOf(12.35), BigDecimal.valueOf(0.52), BigDecimal.valueOf(12.87),
BigDecimal.valueOf(50.40));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(4), 4, LocalDate.of(2024, 3, 1),
LocalDate.of(2024, 4, 1), BigDecimal.valueOf(12.45), BigDecimal.valueOf(0.42), BigDecimal.valueOf(12.87),
BigDecimal.valueOf(37.95));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(5), 5, LocalDate.of(2024, 4, 1),
LocalDate.of(2024, 5, 1), BigDecimal.valueOf(12.55), BigDecimal.valueOf(0.32), BigDecimal.valueOf(12.87),
BigDecimal.valueOf(25.40));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(6), 6, LocalDate.of(2024, 5, 1),
LocalDate.of(2024, 6, 1), BigDecimal.valueOf(12.66), BigDecimal.valueOf(0.21), BigDecimal.valueOf(12.87),
BigDecimal.valueOf(12.74));
checkRepaymentPeriod((LoanSchedulePlanRepaymentPeriod) loanSchedule.getPeriods().get(7), 7, LocalDate.of(2024, 6, 1),
LocalDate.of(2024, 7, 1), BigDecimal.valueOf(12.74), BigDecimal.valueOf(0.11), BigDecimal.valueOf(12.85), BigDecimal.ZERO);
}
private void checkDisbursementPeriod(LoanSchedulePlanDisbursementPeriod period, LocalDate disbursementDate, BigDecimal principalAmount,
BigDecimal outstandingAmount) {
assertEquals(disbursementDate, period.getPeriodFromDate());
assertEquals(disbursementDate, period.getPeriodDueDate());
assertEquals(0, principalAmount.compareTo(period.getPrincipalAmount()));
assertEquals(0, outstandingAmount.compareTo(period.getOutstandingLoanBalance()));
}
private void checkRepaymentPeriod(LoanSchedulePlanRepaymentPeriod period, int expectedPeriodNumber, LocalDate expectedFromDate,
LocalDate expectedDueDate, BigDecimal expectedPrincipalDue, BigDecimal expectedInterestDue, BigDecimal expectedTotalDue,
BigDecimal expectedOutstandingLoanBalance) {
LoanScheduleModelRepaymentPeriod repaymentPeriod = (LoanScheduleModelRepaymentPeriod) period;
assertEquals(expectedPeriodNumber, repaymentPeriod.getPeriodNumber());
assertEquals(expectedFromDate, repaymentPeriod.getFromDate());
assertEquals(expectedDueDate, repaymentPeriod.getDueDate());
assertEquals(0, expectedPrincipalDue.compareTo(repaymentPeriod.getPrincipalDue().getAmount()));
assertEquals(0, expectedInterestDue.compareTo(repaymentPeriod.getInterestDue().getAmount()));
assertEquals(0, expectedTotalDue.compareTo(repaymentPeriod.getTotalDue().getAmount()));
assertEquals(0, expectedOutstandingLoanBalance.compareTo(repaymentPeriod.getOutstandingLoanBalance().getAmount()));
assertEquals(expectedPeriodNumber, period.getPeriodNumber());
assertEquals(expectedFromDate, period.getPeriodFromDate());
assertEquals(expectedDueDate, period.getPeriodDueDate());
assertEquals(0, expectedPrincipalDue.compareTo(period.getPrincipalAmount()));
assertEquals(0, expectedInterestDue.compareTo(period.getInterestAmount()));
assertEquals(0, expectedTotalDue.compareTo(period.getTotalDueAmount()));
assertEquals(0, expectedOutstandingLoanBalance.compareTo(period.getOutstandingLoanBalance()));
}
private void checkDownPaymentPeriod(LoanSchedulePlanDownPaymentPeriod period, int expectedPeriodNumber, LocalDate expectedFromDate,
LocalDate expectedDueDate, BigDecimal expectedPrincipalDue, BigDecimal expectedTotalDue,
BigDecimal expectedOutstandingLoanBalance) {
assertEquals(expectedPeriodNumber, period.getPeriodNumber());
assertEquals(expectedFromDate, period.getPeriodFromDate());
assertEquals(expectedDueDate, period.getPeriodDueDate());
assertEquals(0, expectedPrincipalDue.compareTo(period.getPrincipalAmount()));
assertEquals(0, expectedTotalDue.compareTo(period.getTotalDueAmount()));
assertEquals(0, expectedOutstandingLoanBalance.compareTo(period.getOutstandingLoanBalance()));
}
}