This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit fa5be3f5a53a6f8180099f68f52545de26ce5daa Author: Soma Sörös <[email protected]> AuthorDate: Wed Nov 26 13:46:58 2025 +0100 FINERACT-2354: [BE] chargeback handling with backdated re-age for last adjustment strategy with equal amortization re-aging behaviour --- .../domain/LoanRepaymentScheduleInstallment.java | 6 +- ...dvancedPaymentScheduleTransactionProcessor.java | 66 +++---- .../loanproduct/calc/ProgressiveEMICalculator.java | 65 +++---- .../calc/data/EqualAmortizationValues.java | 27 ++- .../loanproduct/calc/data/RepaymentPeriod.java | 22 ++- .../calc/ProgressiveEMICalculatorTest.java | 201 ++++++++++++++++++++- 6 files changed, 293 insertions(+), 94 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 97999777f6..40590d9cdc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -28,6 +28,7 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -518,8 +519,9 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa return this.installmentNumber.compareTo(o.installmentNumber); } - public int compareToByDueDate(LoanRepaymentScheduleInstallment o) { - return this.dueDate.compareTo(o.dueDate); + public int compareToByFromDueDate(LoanRepaymentScheduleInstallment o) { + return Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate) + .thenComparing(LoanRepaymentScheduleInstallment::getFromDate).compare(this, o); } public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 6362eeea8b..accaa6da30 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -3171,7 +3171,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) { final AtomicInteger counter = new AtomicInteger(1); final AtomicReference<LocalDate> previousDueDate = new AtomicReference<>(null); - installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate).forEachOrdered(i -> { + installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByFromDueDate).forEachOrdered(i -> { i.updateInstallmentNumber(counter.getAndIncrement()); final LocalDate prev = previousDueDate.get(); if (prev != null && (i.isAdditional() || i.isReAged())) { @@ -3179,6 +3179,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } previousDueDate.set(i.getDueDate()); }); + installments.sort(LoanRepaymentScheduleInstallment::compareToByFromDueDate); } private LocalDate calculateReAgedInstallmentDueDate(final LoanReAgeParameter reAgeParameter, final LocalDate dueDate) { @@ -3479,12 +3480,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep BalancesWithPaidInAdvance paidInAdvanceBalances = liftEarlyRepaidBalances(installments, transactionDate, currency, ctx.getAlreadyProcessedTransactions()); - // TODO add as Parameter here: paidInAdvanceBalances.getAggregatedFeeChargesPortion().isGreaterThanZero() || - // paidInAdvanceBalances.getAggregatedPenaltyChargesPortion().isGreaterThanZero() emiCalculator.reAgeEqualAmortization(model, transactionDate, loanReAgeParameter, - outstandingBalances.fees.add(outstandingBalances.penalties), - new EqualAmortizationValues(calculatedFees.value().add(calculatedPenalties.value()), - calculatedFees.adjustment().add(calculatedPenalties.adjustment()))); + outstandingBalances.fees.add(outstandingBalances.penalties), calculatedFees.add(calculatedPenalties)); installments.removeIf(i -> (i.getInstallmentNumber() != null && !i.isDownPayment() && !i.getDueDate().isBefore(transactionDate) && !i.isAdditional()) || (!i.getDueDate().isAfter(model.getMaturityDate()) && i.isAdditional())); @@ -3494,6 +3491,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep i.setInstallmentNumber(model.repaymentPeriods().size()); }); + int reAgedInstallmentIndex = 0; for (int index = 0; index < model.repaymentPeriods().size(); index++) { RepaymentPeriod rp = model.repaymentPeriods().get(index); if (rp.getDueDate().isBefore(transactionDate)) { @@ -3504,9 +3502,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep installment.setInterestCharged(installment.getInterestPaid()); installment.setPrincipal(installment.getPrincipalCompleted(currency).getAmount()); installment.setInstallmentNumber(index + 1); + installment.setCreditedPrincipal(rp.getCreditedPrincipal().getAmount()); installment.updateObligationsMet(currency, transactionDate); - // TODO add remaining components } else { LoanRepaymentScheduleInstallment created = LoanRepaymentScheduleInstallment.newReAgedInstallment(loanTransaction.getLoan(), index + 1, rp.getFromDate(), rp.getDueDate(), rp.getDuePrincipal().getAmount(), rp.getDueInterest().getAmount(), @@ -3525,16 +3523,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep paidInAdvanceBalances.loanTransactionToRepaymentScheduleMappings.forEach(m -> m.setInstallment(created)); } else { - boolean isLastRepaymentPeriod = model.isLastRepaymentPeriod(rp); - created.setFeeChargesCharged(calculatedFees.calculateValueBigDecimal(isLastRepaymentPeriod)); - created.setPenaltyCharges(calculatedPenalties.calculateValueBigDecimal(isLastRepaymentPeriod)); + created.setFeeChargesCharged(calculatedFees.calculateValueBigDecimal(reAgedInstallmentIndex)); + created.setPenaltyCharges(calculatedPenalties.calculateValueBigDecimal(reAgedInstallmentIndex)); - created.setInterestAccrued(calculatedInterestAccrued.calculateValueBigDecimal(isLastRepaymentPeriod)); - created.setFeeAccrued(calculatedFeeAccrued.calculateValueBigDecimal(isLastRepaymentPeriod)); - created.setPenaltyAccrued(calculatedPenaltyAccrued.calculateValueBigDecimal(isLastRepaymentPeriod)); + created.setInterestAccrued(calculatedInterestAccrued.calculateValueBigDecimal(reAgedInstallmentIndex)); + created.setFeeAccrued(calculatedFeeAccrued.calculateValueBigDecimal(reAgedInstallmentIndex)); + created.setPenaltyAccrued(calculatedPenaltyAccrued.calculateValueBigDecimal(reAgedInstallmentIndex)); - createChargeMappingsForInstallment(created, calculatedCharges, isLastRepaymentPeriod); + createChargeMappingsForInstallment(created, calculatedCharges, reAgedInstallmentIndex); + reAgedInstallmentIndex++; } + created.setCreditedPrincipal(rp.getCreditedPrincipal().getAmount()); created.updateObligationsMet(currency, transactionDate); installments.add(created); } @@ -3549,6 +3548,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments(); LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter(); LocalDate transactionDate = loanTransaction.getTransactionDate(); + LocalDate originalMaturityDate = installments.stream() + .filter(i -> !i.isDownPayment() && !i.isAdditional() && i.getDueDate() != null) + .map(LoanRepaymentScheduleInstallment::getDueDate).max(LocalDate::compareTo).orElseThrow(); Integer numberOfReAgeInstallments = loanReAgeParameter.getNumberOfInstallments(); Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail() @@ -3613,8 +3615,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep return res; }).reduce(new BalancesWithPaidInAdvance(currency), BalancesWithPaidInAdvance::summarizerAccumulator); - if (!balances.getPrincipal().isZero() || !balances.getInterest().isZero() || !balances.getFee().isZero() - || !balances.getPenalty().isZero()) { + if (!transactionDate.isAfter(originalMaturityDate)) { final LoanRepaymentScheduleInstallment earlyRepaidInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, firstReAgeInstallmentProps.reAgedInstallmentNumber(), firstReAgeInstallmentProps.fromDate(), transactionDate, @@ -3634,9 +3635,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep InstallmentProcessingHelper.addOneToInstallmentNumberFromInstallment(installments, earlyRepaidInstallment.getInstallmentNumber()); - loan.getRepaymentScheduleInstallments().add(earlyRepaidInstallment); + installments.add(earlyRepaidInstallment); } + // installment index which excludes earlyRepaidInstallment intallment index. + Integer reAgedInstallmentIndex = 0; LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, firstReAgeInstallmentProps.reAgedInstallmentNumber, firstReAgeInstallmentProps.fromDate, loanReAgeParameter.getStartDate(), calculatedPrincipal.value().getAmount(), calculatedInterest.value().getAmount(), calculatedFees.value().getAmount(), @@ -3644,27 +3647,27 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep calculatedFeeAccrued.value().getAmount(), calculatedPenaltyAccrued.value().getAmount()); reAgedInstallment = insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, transactionDate); - createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, false); - + createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, reAgedInstallmentIndex); + reAgedInstallmentIndex++; for (int i = 1; i < numberOfReAgeInstallments; i++) { LocalDate calculatedDueDate = scheduledDateGenerator.getRepaymentPeriodDate(loanReAgeParameter.getFrequencyType(), loanReAgeParameter.getFrequencyNumber(), reAgedInstallment.getDueDate()); calculateReAgedInstallmentDueDate(loanReAgeParameter, reAgedInstallment.getDueDate()); int nextReAgedInstallmentNumber = firstReAgeInstallmentProps.reAgedInstallmentNumber + i; - boolean isLastInstallment = i + 1 == numberOfReAgeInstallments; reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(), nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate, - calculatedPrincipal.calculateValueBigDecimal(isLastInstallment), - calculatedInterest.calculateValueBigDecimal(isLastInstallment), - calculatedFees.calculateValueBigDecimal(isLastInstallment), - calculatedPenalties.calculateValueBigDecimal(isLastInstallment), - calculatedInterestAccrued.calculateValueBigDecimal(isLastInstallment), - calculatedFeeAccrued.calculateValueBigDecimal(isLastInstallment), - calculatedPenaltyAccrued.calculateValueBigDecimal(isLastInstallment)); + calculatedPrincipal.calculateValueBigDecimal(reAgedInstallmentIndex), + calculatedInterest.calculateValueBigDecimal(reAgedInstallmentIndex), + calculatedFees.calculateValueBigDecimal(reAgedInstallmentIndex), + calculatedPenalties.calculateValueBigDecimal(reAgedInstallmentIndex), + calculatedInterestAccrued.calculateValueBigDecimal(reAgedInstallmentIndex), + calculatedFeeAccrued.calculateValueBigDecimal(reAgedInstallmentIndex), + calculatedPenaltyAccrued.calculateValueBigDecimal(reAgedInstallmentIndex)); reAgedInstallment = insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, transactionDate); - createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, isLastInstallment); + createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, reAgedInstallmentIndex); + reAgedInstallmentIndex++; } int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber(); List<LoanRepaymentScheduleInstallment> toRemove = installments.stream() @@ -3676,11 +3679,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } private void createChargeMappingsForInstallment(final LoanRepaymentScheduleInstallment installment, - List<ReAgedChargeEqualAmortizationValues> reAgedChargeEqualAmortizationValues, boolean isLastInstallment) { + List<ReAgedChargeEqualAmortizationValues> reAgedChargeEqualAmortizationValues, Integer index) { reAgedChargeEqualAmortizationValues.forEach(amortizationValue -> { - installment.getInstallmentCharges() - .add(new LoanInstallmentCharge(amortizationValue.equalAmortizationValues.calculateValueBigDecimal(isLastInstallment), - amortizationValue.charge, installment)); + installment.getInstallmentCharges().add(new LoanInstallmentCharge( + amortizationValue.equalAmortizationValues.calculateValueBigDecimal(index), amortizationValue.charge, installment)); }); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index 21a9af44c6..581198918a 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -857,7 +857,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated()); } rp.setEmi(rp.getTotalPaidAmount()); - rp.setOutstandingMoved(true); + rp.moveOutstandingDueToReAging(); }); } @@ -1697,15 +1697,14 @@ public final class ProgressiveEMICalculator implements EMICalculator { EqualAmortizationValues principalEAV = calculateAdjustedEqualAmortizationValues(principal, principal.add(interest).add(feesPenaltiesOutstanding), interestEAV.value().add(feesPenaltiesEqualAmortizationValues.value()), repaymentPeriods.size(), null, currency); - RepaymentPeriod last = repaymentPeriods.getLast(); EqualAmortizationValues emiAEV = principalEAV.add(interestEAV); - repaymentPeriods.forEach(rp -> { - boolean isLast = last.equals(rp); - rp.setReAgedInterest(interestEAV.calculateValue(isLast)); - Money emi = emiAEV.calculateValue(isLast); + for (int i = 0; i < repaymentPeriods.size(); i++) { + RepaymentPeriod rp = repaymentPeriods.get(i); + rp.setReAgedInterest(interestEAV.calculateValue(i)); + Money emi = emiAEV.calculateValue(i); rp.setEmi(emi); rp.setOriginalEmi(emi); - }); + } } @Override @@ -1722,40 +1721,46 @@ public final class ProgressiveEMICalculator implements EMICalculator { LoanReAgeParameter reageParameter, Money feesPenaltiesOutstanding, EqualAmortizationValues feesPenaltiesEqualAmortizationValues) { LocalDate originalMaturityDate = interestSchedule.getMaturityDate(); - boolean isAfterOriginalMaturityDate = transactionDate.isAfter(originalMaturityDate); List<RepaymentPeriod> reAgedRepaymentPeriods = new ArrayList<>(reageParameter.getNumberOfInstallments()); OutstandingDetails reAgeingAmounts = precalculateReAgeEqualAmortizationAmount(interestSchedule, transactionDate, reageParameter); + Money zero = interestSchedule.zero(); // calculate already paid balances from transaction date OutstandingDetails paidBalancesFromTransactionDate = calculatePaidBalancesAfterDate(interestSchedule, transactionDate); + // find future chargebacks. + // find future balance corrections + Money futureCreditedPrincipals = interestSchedule.repaymentPeriods().stream() + .filter(rp -> !rp.getFromDate().isBefore(transactionDate)).filter(rp -> rp.getDueDate().isAfter(transactionDate)) + .map(RepaymentPeriod::getCreditedPrincipal).reduce(zero, Money::add); + // set maturity date to transaction date and remove all repayment periods after it. accelerateMaturityDateTo(interestSchedule, transactionDate); + addCredit(interestSchedule, transactionDate, futureCreditedPrincipals, zero); + // close all open repayment period while keep paid amounts interestSchedule.repaymentPeriods().forEach(rp -> { rp.getInterestPeriods().getLast() .addCreditedInterestAmount(MathUtil.min(rp.getOutstandingInterest(), rp.getCreditedInterest(), false).negated()); rp.setEmi(rp.getTotalPaidAmount()); - rp.setOutstandingMoved(true); + rp.moveOutstandingDueToReAging(); }); // stop calculate unrecognised interest at this point because all interestSchedule.getLastRepaymentPeriod().setNoUnrecognisedInterest(true); - if (!paidBalancesFromTransactionDate.getOutstandingInterest().isZero() - || !paidBalancesFromTransactionDate.getOutstandingPrincipal().isZero()) { + if (!originalMaturityDate.isBefore(transactionDate)) { createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(interestSchedule, paidBalancesFromTransactionDate.getOutstandingPrincipal(), paidBalancesFromTransactionDate.getOutstandingInterest(), true); - addFirstReAgedPeriod(interestSchedule, interestSchedule.getLastRepaymentPeriod()); } - updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods, isAfterOriginalMaturityDate); + updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods); updateEMIForReAgeEqualAmortization(reAgedRepaymentPeriods, reAgeingAmounts.getOutstandingPrincipal(), reAgeingAmounts.getOutstandingInterest(), feesPenaltiesOutstanding, feesPenaltiesEqualAmortizationValues, - interestSchedule.zero().getCurrency()); + zero.getCurrency()); calculateOutstandingBalance(interestSchedule); @@ -1767,31 +1772,17 @@ public final class ProgressiveEMICalculator implements EMICalculator { } private void updateModelForReageEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule, - LoanReAgeParameter reageParameter, List<RepaymentPeriod> reAgedRepaymentPeriods, boolean isAfterOriginalMaturityDate) { + LoanReAgeParameter reageParameter, List<RepaymentPeriod> reAgedRepaymentPeriods) { int numberOfInstallmentsToAdd = reageParameter.getNumberOfInstallments(); LocalDate toDate = reageParameter.getStartDate(); RepaymentPeriod previous = interestSchedule.getLastRepaymentPeriod(); int frequency = reageParameter.getFrequencyNumber(); PeriodFrequencyType frequencyType = reageParameter.getFrequencyType(); - if (!isAfterOriginalMaturityDate) { - // merge first reaged period - RepaymentPeriod firstReAgedPeriod = interestSchedule.getLastRepaymentPeriod(); - firstReAgedPeriod.setDueDate(toDate); - firstReAgedPeriod.getLastInterestPeriod().setDueDate(toDate); - firstReAgedPeriod.setReAged(true); - firstReAgedPeriod.getPrevious().ifPresent(prev -> prev.setNoUnrecognisedInterest(true)); - reAgedRepaymentPeriods.add(firstReAgedPeriod); - - // update params for next reage repayment period calculation - numberOfInstallmentsToAdd--; - toDate = scheduledDateGenerator.getRepaymentPeriodDate(frequencyType, frequency, toDate); - } - // insert new reaged repayment periods for (int i = 0; i < numberOfInstallmentsToAdd; i++) { RepaymentPeriod repaymentPeriod = RepaymentPeriod.create(previous, previous.getDueDate(), toDate, interestSchedule.zero(), - previous.getMc(), previous.getLoanProductRelatedDetail()); + interestSchedule.mc(), previous.getLoanProductRelatedDetail()); repaymentPeriod.setTotalCapitalizedIncomeAmount(previous.getTotalCapitalizedIncomeAmount()); repaymentPeriod.setTotalDisbursedAmount(previous.getTotalDisbursedAmount()); repaymentPeriod.setReAged(true); @@ -1818,14 +1809,6 @@ public final class ProgressiveEMICalculator implements EMICalculator { calculateRateFactorForRepaymentPeriod(targetPeriod, interestSchedule); } - private static void addFirstReAgedPeriod(ProgressiveLoanInterestScheduleModel interestSchedule, RepaymentPeriod targetPeriod) { - RepaymentPeriod repaymentPeriodToInsert = RepaymentPeriod.create(targetPeriod, targetPeriod.getDueDate(), - interestSchedule.getMaturityDate(), interestSchedule.zero(), interestSchedule.mc(), - interestSchedule.loanProductRelatedDetail()); - repaymentPeriodToInsert.setReAged(true); - interestSchedule.repaymentPeriods().add(repaymentPeriodToInsert); - } - private OutstandingDetails calculatePaidBalancesAfterDate(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate) { Money principal = interestSchedule.repaymentPeriods().stream().filter(rp -> !rp.getDueDate().isBefore(transactionDate)) @@ -1844,9 +1827,9 @@ public final class ProgressiveEMICalculator implements EMICalculator { equalMonthlyValue = Money.roundToMultiplesOf(equalMonthlyValue, installmentAmountInMultiplesOf); } Money adjustmentForLastInstallment = totalOutstanding.minus(equalMonthlyValue.multipliedBy(numberOfInstallments)); - return new EqualAmortizationValues(equalMonthlyValue, adjustmentForLastInstallment); + return new EqualAmortizationValues(totalOutstanding, numberOfInstallments, equalMonthlyValue, adjustmentForLastInstallment); } - return new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)); + return new EqualAmortizationValues(totalOutstanding, numberOfInstallments, Money.zero(currency), Money.zero(currency)); } @Override @@ -1857,6 +1840,6 @@ public final class ProgressiveEMICalculator implements EMICalculator { installmentAmountInMultiplesOf, currency); Money value = calculatedEMI.value().minus(sumOfOtherEqualAmortizationValues); Money adjust = outstanding.minus(value.multipliedBy(numberOfInstallments)); - return new EqualAmortizationValues(value, adjust); + return new EqualAmortizationValues(outstanding, numberOfInstallments, value, adjust); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EqualAmortizationValues.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EqualAmortizationValues.java index b97b99e20b..1d4331969c 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EqualAmortizationValues.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EqualAmortizationValues.java @@ -19,23 +19,38 @@ package org.apache.fineract.portfolio.loanproduct.calc.data; import java.math.BigDecimal; +import java.util.Objects; import org.apache.fineract.organisation.monetary.domain.Money; -public record EqualAmortizationValues(Money value, Money adjustment) { +public record EqualAmortizationValues(Money totalOutstanding, Integer numberOfInstallments, Money value, Money adjustment) { public Money getAdjustedValue() { return value.add(adjustment); } - public Money calculateValue(boolean isLast) { - return (isLast ? getAdjustedValue() : value); + /** + * calculates value according to the index of the installments + * + * @param index + * index accepted 0 to number of (installments - 1) + * @return calculated value for the given index + */ + public Money calculateValue(Integer index) { + if (getAdjustedValue().isLessThanZero()) { + return totalOutstanding.minus(value.multipliedBy(index + 1)).isLessThanZero() ? value.zero() : value; + } + return (index == numberOfInstallments - 1 ? getAdjustedValue() : value); } - public BigDecimal calculateValueBigDecimal(boolean isLast) { - return calculateValue(isLast).getAmount(); + public BigDecimal calculateValueBigDecimal(Integer index) { + return calculateValue(index).getAmount(); } public EqualAmortizationValues add(EqualAmortizationValues other) { - return new EqualAmortizationValues(value.add(other.value), adjustment.add(other.adjustment)); + if (!Objects.equals(numberOfInstallments, other.numberOfInstallments)) { + throw new RuntimeException("Incompatible EqualAmortizationValues. numberOfInstallments parameter should match."); + } + return new EqualAmortizationValues(totalOutstanding.add(other.totalOutstanding), numberOfInstallments, value.add(other.value), + adjustment.add(other.adjustment)); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java index 61b9bb7d6d..755f47a74d 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java @@ -91,8 +91,10 @@ public class RepaymentPeriod { @Getter @Setter - private boolean isOutstandingMoved = false; - + private Money creditedPrincipalMovedDueReAge; + @Getter + @Setter + private Money creditedInterestMovedDueReAge; @Setter @Getter private boolean noUnrecognisedInterest; @@ -125,6 +127,8 @@ public class RepaymentPeriod { this.reAged = reAged; this.reAgedEarlyRepaymentHolder = reAgedEarlyRepaymentHolder; this.reAgedInterest = reAgedInterest; + this.creditedInterestMovedDueReAge = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc); + this.creditedInterestMovedDueReAge = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc); } public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc, ILoanConfigurationDetails loanProductRelatedDetail) { @@ -148,7 +152,8 @@ public class RepaymentPeriod { repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest()); - newRepaymentPeriod.setOutstandingMoved(repaymentPeriod.isOutstandingMoved()); + newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge()); + newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge()); // There is always at least 1 interest period, by default with same from-due date as repayment period for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) { newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc)); @@ -162,7 +167,8 @@ public class RepaymentPeriod { repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero, zero, mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest()); - newRepaymentPeriod.setOutstandingMoved(repaymentPeriod.isOutstandingMoved()); + newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge()); + newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge()); // There is always at least 1 interest period, by default with same from-due date as repayment period for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) { var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod); @@ -311,7 +317,8 @@ public class RepaymentPeriod { * @return */ public Money getTotalCreditedAmount() { - return isOutstandingMoved ? Money.zero(getCurrency(), getMc()) : getCreditedPrincipal().plus(getCreditedInterest(), getMc()); + return getCreditedPrincipal().plus(getCreditedInterest(), getMc()).minus(getCreditedInterestMovedDueReAge(), getMc()) + .minus(getCreditedPrincipalMovedDueReAge(), getMc()); } /** @@ -480,4 +487,9 @@ public class RepaymentPeriod { public Money getTotalCapitalizedIncomeAmount() { return MathUtil.nullToZero(totalCapitalizedIncomeAmount, getCurrency(), getMc()); } + + public void moveOutstandingDueToReAging() { + setCreditedPrincipalMovedDueReAge(getCreditedPrincipal()); + setCreditedInterestMovedDueReAge(getCreditedInterest()); + } } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index eef9f3d00b..819eb52b1f 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -31,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; @@ -4239,6 +4240,147 @@ class ProgressiveEMICalculatorTest { @Nested public class ReAgeEqualAmortization { + private ProgressiveLoanInterestScheduleModel generateSchedule() { + final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>(); + + expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); + expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1))); + expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1))); + expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); + expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); + expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(15.678); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); + + checkPeriod(interestSchedule, 0, 0, 17.43, 0.0, 0.0, 1.33, 16.1, 83.9); + checkPeriod(interestSchedule, 0, 1, 17.43, 0.013315561644, 1.3315561644, 1.33, 16.1, 83.9); + checkPeriod(interestSchedule, 1, 0, 17.43, 0.012456493151, 1.04509977537, 1.05, 16.38, 67.52); + checkPeriod(interestSchedule, 2, 0, 17.43, 0.013315561644, 0.899066722202, 0.90, 16.53, 50.99); + checkPeriod(interestSchedule, 3, 0, 17.43, 0.012886027397, 0.657058536972, 0.66, 16.77, 34.22); + checkPeriod(interestSchedule, 4, 0, 17.43, 0.013315561644, 0.455658519458, 0.46, 16.97, 17.25); + checkPeriod(interestSchedule, 5, 0, 17.47, 0.012886027397, 0.222283972598, 0.22, 17.25, 0.0); + return interestSchedule; + } + + @Test + public void test_chargeBackOn2ndRP_ReAgeingOn1stRPsDueDate_EQUAL_AMORTIZATION_FULL_INTEREST() { + ProgressiveLoanInterestScheduleModel interestSchedule = generateSchedule(); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), + Money.of(currency, BigDecimal.valueOf(16.1))); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), + Money.of(currency, BigDecimal.valueOf(1.33))); + + emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), Money.of(currency, BigDecimal.valueOf(17.43))); + + // No repayment no interest recalculation + LocalDate reAgingStartDate = LocalDate.of(2024, 2, 1); + LocalDate transactionDate = LocalDate.of(2024, 2, 1); + + OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, + interestSchedule.getMaturityDate()); + + LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6, + LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null); + + // Update the existing model with re-aged periods + emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); + + OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, + interestSchedule.getMaturityDate()); + + Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(), + outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount()); + Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(), + outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount()); + + } + + @Test + public void test_chargeBackOn2ndRP_ReAgeingOn2stRPsDueDate_EQUAL_AMORTIZATION_FULL_INTEREST() { + ProgressiveLoanInterestScheduleModel interestSchedule = generateSchedule(); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), + Money.of(currency, BigDecimal.valueOf(16.1))); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), + Money.of(currency, BigDecimal.valueOf(1.33))); + + emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), Money.of(currency, BigDecimal.valueOf(17.43))); + + // No repayment no interest recalculation + LocalDate reAgingStartDate = LocalDate.of(2024, 3, 1); + LocalDate transactionDate = LocalDate.of(2024, 3, 1); + + OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, + interestSchedule.getMaturityDate()); + + LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6, + LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null); + + // Update the existing model with re-aged periods + emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); + + OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, + interestSchedule.getMaturityDate()); + + Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(), + outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount()); + Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(), + outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount()); + + } + + @Test + public void test_chargeBackOn2ndRP_ReAgeingDuring2stRP_EQUAL_AMORTIZATION_FULL_INTEREST() { + ProgressiveLoanInterestScheduleModel interestSchedule = generateSchedule(); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), + Money.of(currency, BigDecimal.valueOf(16.1))); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), + Money.of(currency, BigDecimal.valueOf(1.33))); + + emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), Money.of(currency, BigDecimal.valueOf(17.43))); + + // No repayment no interest recalculation + LocalDate reAgingStartDate = LocalDate.of(2024, 2, 10); + LocalDate transactionDate = LocalDate.of(2024, 2, 10); + + OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, + interestSchedule.getMaturityDate()); + + LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6, + LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null); + + // Update the existing model with re-aged periods + emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); + + OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, + interestSchedule.getMaturityDate()); + + Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(), + outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount()); + Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(), + outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount()); + + } + @Test public void test_transactionInMiddleOfPeriod_EQUAL_AMORTIZATION_FULL_INTEREST_noTransactionTilDate_noInterestRecalc() { final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>(); @@ -4291,7 +4433,7 @@ class ProgressiveEMICalculatorTest { // Update the existing model with re-aged periods emiCalculator.reAgeEqualAmortization(interestSchedule, loanTransaction.getTransactionDate(), reageParameter, - Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), Money.zero(currency))); + Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, interestSchedule.getMaturityDate()); @@ -4380,7 +4522,7 @@ class ProgressiveEMICalculatorTest { // Update the existing model with re-aged periods emiCalculator.reAgeEqualAmortization(interestSchedule, loanTransaction.getTransactionDate(), reageParameter, - Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), Money.zero(currency))); + Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, interestSchedule.getMaturityDate()); @@ -4439,7 +4581,7 @@ class ProgressiveEMICalculatorTest { // Update the existing model with re-aged periods emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), - new EqualAmortizationValues(Money.zero(currency), Money.zero(currency))); + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, interestSchedule.getMaturityDate()); @@ -4499,7 +4641,7 @@ class ProgressiveEMICalculatorTest { // Update the existing model with re-aged periods emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), - new EqualAmortizationValues(Money.zero(currency), Money.zero(currency))); + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, interestSchedule.getMaturityDate()); @@ -4582,7 +4724,7 @@ class ProgressiveEMICalculatorTest { // Update the existing model with re-aged periods emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), - new EqualAmortizationValues(Money.zero(currency), Money.zero(currency))); + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, interestSchedule.getMaturityDate()); @@ -4665,7 +4807,7 @@ class ProgressiveEMICalculatorTest { // Update the existing model with re-aged periods emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), - new EqualAmortizationValues(Money.zero(currency), Money.zero(currency))); + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, interestSchedule.getMaturityDate()); @@ -4738,7 +4880,7 @@ class ProgressiveEMICalculatorTest { // Update the existing model with re-aged periods emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), - new EqualAmortizationValues(Money.zero(currency), Money.zero(currency))); + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, interestSchedule.getMaturityDate()); @@ -4811,7 +4953,7 @@ class ProgressiveEMICalculatorTest { // Update the existing model with re-aged periods emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency), - new EqualAmortizationValues(Money.zero(currency), Money.zero(currency))); + new EqualAmortizationValues(Money.zero(currency), 6, Money.zero(currency), Money.zero(currency))); OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, interestSchedule.getMaturityDate()); @@ -4824,6 +4966,49 @@ class ProgressiveEMICalculatorTest { } } + @Nested + public class EqualAmortizationValue { + + @Test + public void test_AmortizationTotalIsLessThanInstallmentNumber() { + EqualAmortizationValues actual = emiCalculator.calculateEqualAmortizationValues(Money.of(currency, BigDecimal.valueOf(0.04)), 6, + null, MonetaryCurrency.fromCurrencyData(currency)); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(0).doubleValue()); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(1).doubleValue()); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(2).doubleValue()); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(3).doubleValue()); + Assertions.assertEquals(0.0, actual.calculateValueBigDecimal(4).doubleValue()); + Assertions.assertEquals(0.0, actual.calculateValueBigDecimal(5).doubleValue()); + + } + + @Test + public void test_AmortizationIsJustBiggerThanInstallmentNumber() { + EqualAmortizationValues actual = emiCalculator.calculateEqualAmortizationValues(Money.of(currency, BigDecimal.valueOf(0.07)), 6, + null, MonetaryCurrency.fromCurrencyData(currency)); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(0).doubleValue()); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(1).doubleValue()); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(2).doubleValue()); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(3).doubleValue()); + Assertions.assertEquals(0.01, actual.calculateValueBigDecimal(4).doubleValue()); + Assertions.assertEquals(0.02, actual.calculateValueBigDecimal(5).doubleValue()); + + } + + @Test + public void test_AmortizationNonEdgeCase() { + EqualAmortizationValues actual = emiCalculator.calculateEqualAmortizationValues(Money.of(currency, BigDecimal.valueOf(0.59)), 6, + null, MonetaryCurrency.fromCurrencyData(currency)); + Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(0).doubleValue()); + Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(1).doubleValue()); + Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(2).doubleValue()); + Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(3).doubleValue()); + Assertions.assertEquals(0.1, actual.calculateValueBigDecimal(4).doubleValue()); + Assertions.assertEquals(0.09, actual.calculateValueBigDecimal(5).doubleValue()); + + } + } + // utilities private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) { return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
