FINERACT-1981: Fix calculation new way when applies multi disbursement
This commit is contained in:
committed by
Adam Saghy
parent
81ed0260b1
commit
b4b9d3ea71
+9
-9
@@ -89,7 +89,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
|
final Money zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
|
||||||
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
|
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
|
||||||
List<RepaymentPeriod> repaymentPeriods = periods.stream().map(e -> {
|
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);
|
prev.set(rp);
|
||||||
return rp;
|
return rp;
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -374,14 +374,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
||||||
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
|
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
|
||||||
final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty()
|
final boolean onlyOnActualModelShouldApply = scheduleModel.isEmpty()
|
||||||
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE;
|
|| operation.getAction() == EmiChangeOperation.Action.INTEREST_RATE_CHANGE || scheduleModel.isCopiedForCalculation();
|
||||||
|
|
||||||
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
|
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
|
||||||
calculateOutstandingBalance(scheduleModel);
|
calculateOutstandingBalance(scheduleModel);
|
||||||
if (onlyOnActualModelShouldApply) {
|
if (onlyOnActualModelShouldApply) {
|
||||||
calculateEMIOnActualModel(relatedRepaymentPeriods, scheduleModel);
|
calculateEMIOnActualModel(relatedRepaymentPeriods, scheduleModel);
|
||||||
} else {
|
} else {
|
||||||
calculateEMIOnNewEmptyModelAndMerge(relatedRepaymentPeriods, scheduleModel, operation);
|
calculateEMIOnNewModelAndMerge(relatedRepaymentPeriods, scheduleModel, operation);
|
||||||
}
|
}
|
||||||
calculateOutstandingBalance(scheduleModel);
|
calculateOutstandingBalance(scheduleModel);
|
||||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
||||||
@@ -733,18 +733,18 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void calculateEMIOnNewEmptyModelAndMerge(List<RepaymentPeriod> repaymentPeriods,
|
private void calculateEMIOnNewModelAndMerge(List<RepaymentPeriod> repaymentPeriods, ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||||
ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) {
|
final EmiChangeOperation operation) {
|
||||||
if (repaymentPeriods.isEmpty()) {
|
if (repaymentPeriods.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.emptyCopy();
|
final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.copyWithoutPaidAmounts();
|
||||||
addDisbursement(scheduleModelCopy, operation);
|
addDisbursement(scheduleModelCopy, operation.withZeroAmount());
|
||||||
|
|
||||||
final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate();
|
final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate();
|
||||||
scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> {
|
scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> {
|
||||||
actualRepaymentPeriod.setEmi(actualRepaymentPeriod.getEmi().plus(newRepaymentPeriod.getEmi()));
|
actualRepaymentPeriod.setEmi(newRepaymentPeriod.getEmi());
|
||||||
actualRepaymentPeriod.setOriginalEmi(actualRepaymentPeriod.getOriginalEmi().plus(newRepaymentPeriod.getOriginalEmi()));
|
actualRepaymentPeriod.setOriginalEmi(newRepaymentPeriod.getOriginalEmi());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
@@ -46,4 +46,11 @@ public class EmiChangeOperation {
|
|||||||
public static EmiChangeOperation changeInterestRate(final LocalDate newInterestSubmittedOnDate, final BigDecimal newInterestRate) {
|
public static EmiChangeOperation changeInterestRate(final LocalDate newInterestSubmittedOnDate, final BigDecimal newInterestRate) {
|
||||||
return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-7
@@ -56,6 +56,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
|||||||
private final Integer installmentAmountInMultiplesOf;
|
private final Integer installmentAmountInMultiplesOf;
|
||||||
private final MathContext mc;
|
private final MathContext mc;
|
||||||
private final Money zero;
|
private final Money zero;
|
||||||
|
private final boolean isCopiedForCalculation;
|
||||||
|
|
||||||
public ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods,
|
public ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods,
|
||||||
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
|
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
|
||||||
@@ -67,33 +68,34 @@ public class ProgressiveLoanInterestScheduleModel {
|
|||||||
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
|
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
|
||||||
this.mc = mc;
|
this.mc = mc;
|
||||||
this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
|
this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
|
||||||
|
this.isCopiedForCalculation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods, final TreeSet<InterestRate> interestRates,
|
private ProgressiveLoanInterestScheduleModel(final List<RepaymentPeriod> repaymentPeriods, final TreeSet<InterestRate> interestRates,
|
||||||
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
|
final LoanProductMinimumRepaymentScheduleRelatedDetail loanProductRelatedDetail,
|
||||||
final Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariations, final Integer installmentAmountInMultiplesOf,
|
final Map<LoanTermVariationType, List<LoanTermVariationsData>> loanTermVariations, final Integer installmentAmountInMultiplesOf,
|
||||||
final MathContext mc) {
|
final MathContext mc, final boolean isCopiedForCalculation) {
|
||||||
this.mc = mc;
|
this.mc = mc;
|
||||||
this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods,
|
this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods,
|
||||||
(previousPeriod, repaymentPeriod) -> new RepaymentPeriod(previousPeriod, repaymentPeriod, mc));
|
(previousPeriod, repaymentPeriod) -> RepaymentPeriod.copy(previousPeriod, repaymentPeriod, mc));
|
||||||
this.interestRates = new TreeSet<>(interestRates);
|
this.interestRates = new TreeSet<>(interestRates);
|
||||||
this.loanProductRelatedDetail = loanProductRelatedDetail;
|
this.loanProductRelatedDetail = loanProductRelatedDetail;
|
||||||
this.loanTermVariations = loanTermVariations;
|
this.loanTermVariations = loanTermVariations;
|
||||||
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
|
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
|
||||||
this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
|
this.zero = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
|
||||||
|
this.isCopiedForCalculation = isCopiedForCalculation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) {
|
public ProgressiveLoanInterestScheduleModel deepCopy(MathContext mc) {
|
||||||
return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates, loanProductRelatedDetail, loanTermVariations,
|
return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates, loanProductRelatedDetail, loanTermVariations,
|
||||||
installmentAmountInMultiplesOf, mc);
|
installmentAmountInMultiplesOf, mc, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProgressiveLoanInterestScheduleModel emptyCopy() {
|
public ProgressiveLoanInterestScheduleModel copyWithoutPaidAmounts() {
|
||||||
final List<RepaymentPeriod> repaymentPeriodCopies = copyRepaymentPeriods(repaymentPeriods,
|
final List<RepaymentPeriod> repaymentPeriodCopies = copyRepaymentPeriods(repaymentPeriods,
|
||||||
(previousPeriod, repaymentPeriod) -> new RepaymentPeriod(previousPeriod, repaymentPeriod.getFromDate(),
|
(previousPeriod, repaymentPeriod) -> RepaymentPeriod.copyWithoutPaidAmounts(previousPeriod, repaymentPeriod, mc));
|
||||||
repaymentPeriod.getDueDate(), repaymentPeriod.getEmi().zero(), mc));
|
|
||||||
return new ProgressiveLoanInterestScheduleModel(repaymentPeriodCopies, interestRates, loanProductRelatedDetail, loanTermVariations,
|
return new ProgressiveLoanInterestScheduleModel(repaymentPeriodCopies, interestRates, loanProductRelatedDetail, loanTermVariations,
|
||||||
installmentAmountInMultiplesOf, mc);
|
installmentAmountInMultiplesOf, mc, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<RepaymentPeriod> copyRepaymentPeriods(final List<RepaymentPeriod> repaymentPeriods,
|
private List<RepaymentPeriod> copyRepaymentPeriods(final List<RepaymentPeriod> repaymentPeriods,
|
||||||
|
|||||||
+39
-20
@@ -37,7 +37,7 @@ import org.apache.fineract.portfolio.util.Memo;
|
|||||||
|
|
||||||
@ToString(exclude = { "previous" })
|
@ToString(exclude = { "previous" })
|
||||||
@EqualsAndHashCode(exclude = { "previous" })
|
@EqualsAndHashCode(exclude = { "previous" })
|
||||||
public class RepaymentPeriod {
|
public final class RepaymentPeriod {
|
||||||
|
|
||||||
private final RepaymentPeriod previous;
|
private final RepaymentPeriod previous;
|
||||||
@Getter
|
@Getter
|
||||||
@@ -59,40 +59,59 @@ public class RepaymentPeriod {
|
|||||||
@Getter
|
@Getter
|
||||||
private Money paidInterest;
|
private Money paidInterest;
|
||||||
|
|
||||||
|
private final MathContext mc;
|
||||||
|
|
||||||
private Memo<BigDecimal> rateFactorPlus1Calculation;
|
private Memo<BigDecimal> rateFactorPlus1Calculation;
|
||||||
private Memo<Money> calculatedDueInterestCalculation;
|
private Memo<Money> calculatedDueInterestCalculation;
|
||||||
private Memo<Money> dueInterestCalculation;
|
private Memo<Money> dueInterestCalculation;
|
||||||
private Memo<Money> outstandingBalanceCalculation;
|
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.previous = previous;
|
||||||
this.fromDate = fromDate;
|
this.fromDate = fromDate;
|
||||||
this.dueDate = dueDate;
|
this.dueDate = dueDate;
|
||||||
|
this.interestPeriods = interestPeriods;
|
||||||
this.emi = emi;
|
this.emi = emi;
|
||||||
this.originalEmi = emi;
|
this.originalEmi = originalEmi;
|
||||||
|
this.paidPrincipal = paidPrincipal;
|
||||||
|
this.paidInterest = paidInterest;
|
||||||
this.mc = mc;
|
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) {
|
public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc) {
|
||||||
this.previous = previous;
|
final Money zero = emi.zero();
|
||||||
this.fromDate = repaymentPeriod.fromDate;
|
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero,
|
||||||
this.dueDate = repaymentPeriod.dueDate;
|
mc);
|
||||||
this.emi = repaymentPeriod.emi;
|
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
||||||
this.originalEmi = repaymentPeriod.originalEmi;
|
newRepaymentPeriod.interestPeriods.add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate));
|
||||||
this.interestPeriods = new ArrayList<>();
|
return newRepaymentPeriod;
|
||||||
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
|
// There is always at least 1 interest period, by default with same from-due date as repayment period
|
||||||
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
|
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() {
|
public Optional<RepaymentPeriod> getPrevious() {
|
||||||
|
|||||||
+11
-11
@@ -934,16 +934,16 @@ class ProgressiveEMICalculatorTest {
|
|||||||
disbursedAmount = toMoney(25.0);
|
disbursedAmount = toMoney(25.0);
|
||||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount);
|
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, 0, 29.94, 0.001019591398, 0.00, 1.15, 28.79, 146.21);
|
||||||
checkPeriod(interestSchedule, 0, 1, 29.93, 0.000764693548, 0.08, 1.15, 28.78, 146.22);
|
checkPeriod(interestSchedule, 0, 1, 29.94, 0.000764693548, 0.08, 1.15, 28.79, 146.21);
|
||||||
checkPeriod(interestSchedule, 0, 2, 29.93, 0.006117548387, 1.07, 1.15, 28.78, 146.22);
|
checkPeriod(interestSchedule, 0, 2, 29.94, 0.006117548387, 1.07, 1.15, 28.79, 146.21);
|
||||||
checkPeriod(interestSchedule, 1, 0, 29.93, 0.007901833333, 1.16, 28.77, 117.45);
|
checkPeriod(interestSchedule, 1, 0, 29.94, 0.007901833333, 1.16, 28.78, 117.43);
|
||||||
checkPeriod(interestSchedule, 2, 0, 29.93, 0.007901833333, 0.93, 29.00, 88.45);
|
checkPeriod(interestSchedule, 2, 0, 29.94, 0.007901833333, 0.93, 29.01, 88.42);
|
||||||
checkPeriod(interestSchedule, 3, 0, 29.93, 0.007901833333, 0.70, 29.23, 59.22);
|
checkPeriod(interestSchedule, 3, 0, 29.94, 0.007901833333, 0.70, 29.24, 59.18);
|
||||||
checkPeriod(interestSchedule, 4, 0, 29.93, 0.007901833333, 0.47, 29.46, 29.76);
|
checkPeriod(interestSchedule, 4, 0, 29.94, 0.007901833333, 0.47, 29.47, 29.71);
|
||||||
checkPeriod(interestSchedule, 5, 0, 30.0, 0.007901833333, 0.24, 29.76, 0.0);
|
checkPeriod(interestSchedule, 5, 0, 29.94, 0.007901833333, 0.23, 29.71, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -1024,8 +1024,8 @@ class ProgressiveEMICalculatorTest {
|
|||||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), toMoney(200.0));
|
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), toMoney(200.0));
|
||||||
checkTotalInterestDue(interestSchedule, 4.11);
|
checkTotalInterestDue(interestSchedule, 4.11);
|
||||||
|
|
||||||
checkEmi(interestSchedule, 4, 85.05);
|
checkEmi(interestSchedule, 4, 85.04);
|
||||||
checkEmi(interestSchedule, 5, 78.86);
|
checkEmi(interestSchedule, 5, 78.91);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user