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 b4b9d3ea714ffefe65a24038f4dd17fbf1fa7dea Author: Janos Meszaros <[email protected]> AuthorDate: Wed Feb 19 21:02:22 2025 +0100 FINERACT-1981: Fix calculation new way when applies multi disbursement --- .../loanproduct/calc/ProgressiveEMICalculator.java | 18 +++---- .../loanproduct/calc/data/EmiChangeOperation.java | 7 +++ .../data/ProgressiveLoanInterestScheduleModel.java | 16 +++--- .../loanproduct/calc/data/RepaymentPeriod.java | 57 ++++++++++++++-------- .../calc/ProgressiveEMICalculatorTest.java | 22 ++++----- 5 files changed, 74 insertions(+), 46 deletions(-) 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 bc2104758..6cf8d0680 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 @@ -89,7 +89,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc); final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>(); List<RepaymentPeriod> repaymentPeriods = periods.stream().map(e -> { - RepaymentPeriod rp = new RepaymentPeriod(prev.get(), from.apply(e), to.apply(e), zero, mc); + RepaymentPeriod rp = RepaymentPeriod.create(prev.get(), from.apply(e), to.apply(e), zero, mc); prev.set(rp); return rp; }).toList(); @@ -374,14 +374,14 @@ public final class ProgressiveEMICalculator implements EMICalculator { final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) { final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate); final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty() - || operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE; + || operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE || scheduleModel.isCopiedForCalculation(); calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel); calculateOutstandingBalance(scheduleModel); if (onlyOnActualModelShouldApply) { calculateEMIOnActualModel(relatedRepaymentPeriods, scheduleModel); } else { - calculateEMIOnNewEmptyModelAndMerge(relatedRepaymentPeriods, scheduleModel, operation); + calculateEMIOnNewModelAndMerge(relatedRepaymentPeriods, scheduleModel, operation); } calculateOutstandingBalance(scheduleModel); calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); @@ -733,18 +733,18 @@ public final class ProgressiveEMICalculator implements EMICalculator { }); } - private void calculateEMIOnNewEmptyModelAndMerge(List<RepaymentPeriod> repaymentPeriods, - ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) { + private void calculateEMIOnNewModelAndMerge(List<RepaymentPeriod> repaymentPeriods, ProgressiveLoanInterestScheduleModel scheduleModel, + final EmiChangeOperation operation) { if (repaymentPeriods.isEmpty()) { return; } - final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.emptyCopy(); - addDisbursement(scheduleModelCopy, operation); + final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.copyWithoutPaidAmounts(); + addDisbursement(scheduleModelCopy, operation.withZeroAmount()); final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate(); scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> { - actualRepaymentPeriod.setEmi(actualRepaymentPeriod.getEmi().plus(newRepaymentPeriod.getEmi())); - actualRepaymentPeriod.setOriginalEmi(actualRepaymentPeriod.getOriginalEmi().plus(newRepaymentPeriod.getOriginalEmi())); + actualRepaymentPeriod.setEmi(newRepaymentPeriod.getEmi()); + actualRepaymentPeriod.setOriginalEmi(newRepaymentPeriod.getOriginalEmi()); }); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java index c25639bee..ea0f12020 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java @@ -46,4 +46,11 @@ public class EmiChangeOperation { public static EmiChangeOperation changeInterestRate(final LocalDate newInterestSubmittedOnDate, final BigDecimal newInterestRate) { return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate); } + + public EmiChangeOperation withZeroAmount() { + if (action == Action.DISBURSEMENT) { + return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null); + } + return null; + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java index ded2ef328..3c79fd41a 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java @@ -56,6 +56,7 @@ public class ProgressiveLoanInterestScheduleModel { private final Integer installmentAmountInMultiplesOf; private final MathContext mc; private final Money zero; + private final boolean isCopiedForCalculation; public ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods, final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, @@ -67,33 +68,34 @@ public class ProgressiveLoanInterestScheduleModel { this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf; this.mc = mc; this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc); + this.isCopiedForCalculation = false; } private ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods, final TreeSet<InterestRate> interestRates, final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail, final Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariations, final Integer installmentAmountInMultiplesOf, - final MathContext mc) { + final MathContext mc, final boolean isCopiedForCalculation) { this.mc = mc; this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods, - (previousPeriod, repaymentPeriod) -> new RepaymentPeriod(previousPeriod, repaymentPeriod, mc)); + (previousPeriod, repaymentPeriod) -> RepaymentPeriod.copy(previousPeriod, repaymentPeriod, mc)); this.interestRates = new TreeSet<>(interestRates); this.loanProductRelatedDetail = loanProductRelatedDetail; this.loanTermVariations = loanTermVariations; this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf; this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc); + this.isCopiedForCalculation = isCopiedForCalculation; } public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) { return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates, loanProductRelatedDetail, loanTermVariations, - installmentAmountInMultiplesOf, mc); + installmentAmountInMultiplesOf, mc, false); } - public ProgressiveLoanInterestScheduleModel emptyCopy() { + public ProgressiveLoanInterestScheduleModel copyWithoutPaidAmounts() { final List<RepaymentPeriod> repaymentPeriodCopies = copyRepaymentPeriods(repaymentPeriods, - (previousPeriod, repaymentPeriod) -> new RepaymentPeriod(previousPeriod, repaymentPeriod.getFromDate(), - repaymentPeriod.getDueDate(), repaymentPeriod.getEmi().zero(), mc)); + (previousPeriod, repaymentPeriod) -> RepaymentPeriod.copyWithoutPaidAmounts(previousPeriod, repaymentPeriod, mc)); return new ProgressiveLoanInterestScheduleModel(repaymentPeriodCopies, interestRates, loanProductRelatedDetail, loanTermVariations, - installmentAmountInMultiplesOf, mc); + installmentAmountInMultiplesOf, mc, true); } private List<RepaymentPeriod> copyRepaymentPeriods(final List<RepaymentPeriod> repaymentPeriods, 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 24edc1144..210ac0a7b 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 @@ -37,7 +37,7 @@ import org.apache.fineract.portfolio.util.Memo; @ToString(exclude = { "previous" }) @EqualsAndHashCode(exclude = { "previous" }) -public class RepaymentPeriod { +public final class RepaymentPeriod { private final RepaymentPeriod previous; @Getter @@ -59,40 +59,59 @@ public class RepaymentPeriod { @Getter private Money paidInterest; + private final MathContext mc; + private Memo<BigDecimal> rateFactorPlus1Calculation; private Memo<Money> calculatedDueInterestCalculation; private Memo<Money> dueInterestCalculation; private Memo<Money> outstandingBalanceCalculation; - private final MathContext mc; - public RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) { + private RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods, + Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, MathContext mc) { this.previous = previous; this.fromDate = fromDate; this.dueDate = dueDate; + this.interestPeriods = interestPeriods; this.emi = emi; - this.originalEmi = emi; + this.originalEmi = originalEmi; + this.paidPrincipal = paidPrincipal; + this.paidInterest = paidInterest; this.mc = mc; - this.interestPeriods = new ArrayList<>(); + } + + public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) { + final Money zero = emi.zero(); + final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero, + mc); // There is always at least 1 interest period, by default with same from-due date as repayment period - getInterestPeriods().add(InterestPeriod.withEmptyAmounts(this, getFromDate(), getDueDate())); - this.paidInterest = getZero(mc); - this.paidPrincipal = getZero(mc); + newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate)); + return newRepaymentPeriod; } - public RepaymentPeriod(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) { - this.previous = previous; - this.fromDate = repaymentPeriod.fromDate; - this.dueDate = repaymentPeriod.dueDate; - this.emi = repaymentPeriod.emi; - this.originalEmi = repaymentPeriod.originalEmi; - this.interestPeriods = new ArrayList<>(); - this.paidPrincipal = repaymentPeriod.paidPrincipal; - this.paidInterest = repaymentPeriod.paidInterest; - this.mc = mc; + public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) { + final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate, + new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, repaymentPeriod.paidPrincipal, + repaymentPeriod.paidInterest, mc); + // There is always at least 1 interest period, by default with same from-due date as repayment period + for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { + newRepaymentPeriod.interestPeriods.add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod)); + } + return newRepaymentPeriod; + } + + public static RepaymentPeriod copyWithoutPaidAmounts(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod, MathContext mc) { + final Money zero = repaymentPeriod.emi.zero(); + final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.fromDate, repaymentPeriod.dueDate, + new ArrayList<>(), repaymentPeriod.emi, repaymentPeriod.originalEmi, zero, zero, mc); // There is always at least 1 interest period, by default with same from-due date as repayment period for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) { - interestPeriods.add(InterestPeriod.copy(this, interestPeriod)); + var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod); + if (!interestPeriodCopy.getBalanceCorrectionAmount().isZero()) { + interestPeriodCopy.addBalanceCorrectionAmount(interestPeriodCopy.getBalanceCorrectionAmount().negated()); + } + newRepaymentPeriod.interestPeriods.add(interestPeriodCopy); } + return newRepaymentPeriod; } public Optional<RepaymentPeriod> getPrevious() { 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 9a40cc386..e7068108e 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 @@ -934,16 +934,16 @@ class ProgressiveEMICalculatorTest { disbursedAmount = toMoney(25.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); - checkTotalInterestDue(interestSchedule, 4.65); + checkTotalInterestDue(interestSchedule, 4.64); - checkPeriod(interestSchedule, 0, 0, 29.93, 0.001019591398, 0.00, 1.15, 28.78, 146.22); - checkPeriod(interestSchedule, 0, 1, 29.93, 0.000764693548, 0.08, 1.15, 28.78, 146.22); - checkPeriod(interestSchedule, 0, 2, 29.93, 0.006117548387, 1.07, 1.15, 28.78, 146.22); - checkPeriod(interestSchedule, 1, 0, 29.93, 0.007901833333, 1.16, 28.77, 117.45); - checkPeriod(interestSchedule, 2, 0, 29.93, 0.007901833333, 0.93, 29.00, 88.45); - checkPeriod(interestSchedule, 3, 0, 29.93, 0.007901833333, 0.70, 29.23, 59.22); - checkPeriod(interestSchedule, 4, 0, 29.93, 0.007901833333, 0.47, 29.46, 29.76); - checkPeriod(interestSchedule, 5, 0, 30.0, 0.007901833333, 0.24, 29.76, 0.0); + checkPeriod(interestSchedule, 0, 0, 29.94, 0.001019591398, 0.00, 1.15, 28.79, 146.21); + checkPeriod(interestSchedule, 0, 1, 29.94, 0.000764693548, 0.08, 1.15, 28.79, 146.21); + checkPeriod(interestSchedule, 0, 2, 29.94, 0.006117548387, 1.07, 1.15, 28.79, 146.21); + checkPeriod(interestSchedule, 1, 0, 29.94, 0.007901833333, 1.16, 28.78, 117.43); + checkPeriod(interestSchedule, 2, 0, 29.94, 0.007901833333, 0.93, 29.01, 88.42); + checkPeriod(interestSchedule, 3, 0, 29.94, 0.007901833333, 0.70, 29.24, 59.18); + checkPeriod(interestSchedule, 4, 0, 29.94, 0.007901833333, 0.47, 29.47, 29.71); + checkPeriod(interestSchedule, 5, 0, 29.94, 0.007901833333, 0.23, 29.71, 0.0); } @Test @@ -1024,8 +1024,8 @@ class ProgressiveEMICalculatorTest { emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), toMoney(200.0)); checkTotalInterestDue(interestSchedule, 4.11); - checkEmi(interestSchedule, 4, 85.05); - checkEmi(interestSchedule, 5, 78.86); + checkEmi(interestSchedule, 4, 85.04); + checkEmi(interestSchedule, 5, 78.91); } @Test
