FINERACT-1981: Fix calculation new way when applies multi disbursement

This commit is contained in:
Janos Meszaros
2025-02-20 14:24:24 +01:00
committed by Adam Saghy
parent 81ed0260b1
commit b4b9d3ea71
5 changed files with 75 additions and 47 deletions
@@ -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());
});
}
@@ -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;
}
}
@@ -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,
@@ -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<>();
// 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);
}
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 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
newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate));
return newRepaymentPeriod;
}
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) {
interestPeriods.add(InterestPeriod.copy(this, interestPeriod));
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) {
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() {
@@ -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