Skip to content

Commit

Permalink
FINERACT-2114: EMI Calculator outstanding balance correction
Browse files Browse the repository at this point in the history
  • Loading branch information
janez89 committed Sep 4, 2024
1 parent a6e3517 commit 54d9e6b
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ public class ProgressiveLoanInterestRepaymentInterestPeriod implements Comparabl
private BigDecimal rateFactorMinus1;

private Money disbursedAmount;
private Money correctionAmount;
private Money interestDue;

public ProgressiveLoanInterestRepaymentInterestPeriod(final ProgressiveLoanInterestRepaymentInterestPeriod period) {
this(period.fromDate, period.dueDate, period.rateFactorMinus1, period.disbursedAmount, period.interestDue);
this(period.fromDate, period.dueDate, period.rateFactorMinus1, period.disbursedAmount, period.correctionAmount, period.interestDue);
}

@Override
Expand All @@ -47,6 +48,14 @@ public int compareTo(@NotNull ProgressiveLoanInterestRepaymentInterestPeriod o)
}

public void addDisbursedAmount(final Money outstandingBalance) {
this.disbursedAmount = this.disbursedAmount.add(outstandingBalance);
if (outstandingBalance != null && !outstandingBalance.isZero()) {
this.disbursedAmount = this.disbursedAmount.add(outstandingBalance);
}
}

public void addCorrectionAmount(final Money correctionAmount) {
if (correctionAmount != null && !correctionAmount.isZero()) {
this.correctionAmount = this.correctionAmount.add(correctionAmount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,34 @@ public class ProgressiveLoanInterestRepaymentModel {

private LinkedList<ProgressiveLoanInterestRepaymentInterestPeriod> interestPeriods;

private boolean isLastPeriod;

private Money equalMonthlyInstallment;
private Money principalDue;
private Money remainingBalance;
private Money outstandingBalance;

public ProgressiveLoanInterestRepaymentModel(final LocalDate fromDate, final LocalDate dueDate, final Money equalMonthlyInstallment) {
this.fromDate = fromDate;
this.dueDate = dueDate;
this.equalMonthlyInstallment = equalMonthlyInstallment;
this.isLastPeriod = false;

final Money zeroAmount = Money.zero(equalMonthlyInstallment.getCurrency());
this.outstandingBalance = zeroAmount;
this.remainingBalance = zeroAmount;
this.principalDue = zeroAmount;
this.interestPeriods = new LinkedList<>();
this.interestPeriods
.add(new ProgressiveLoanInterestRepaymentInterestPeriod(fromDate, dueDate, BigDecimal.ZERO, zeroAmount, zeroAmount));
this.interestPeriods.add(
new ProgressiveLoanInterestRepaymentInterestPeriod(fromDate, dueDate, BigDecimal.ZERO, zeroAmount, zeroAmount, zeroAmount));
}

public ProgressiveLoanInterestRepaymentModel(ProgressiveLoanInterestRepaymentModel repaymentModel) {
this.fromDate = repaymentModel.fromDate;
this.dueDate = repaymentModel.dueDate;
this.isLastPeriod = repaymentModel.isLastPeriod;
this.equalMonthlyInstallment = repaymentModel.equalMonthlyInstallment;
this.outstandingBalance = repaymentModel.outstandingBalance;
this.remainingBalance = repaymentModel.remainingBalance;
this.principalDue = repaymentModel.principalDue;
this.interestPeriods = new LinkedList<>();
Expand All @@ -76,7 +83,12 @@ public Money getInterestDue() {
.reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus);
}

public Money getOutstandingBalance() {
return remainingBalance.plus(principalDue);
public Money getCorrectionAmount() {
return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getCorrectionAmount)
.reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus);
}

public Money getCorrectedOutstandingBalance() {
return outstandingBalance.plus(getCorrectionAmount());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriod(Prog
void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestEffectiveDate,
BigDecimal newInterestRate);

void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount);

Optional<ProgressiveLoanInterestRepaymentModel> getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate date);

ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel);

ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(final
interestRepaymentModelList
.add(new ProgressiveLoanInterestRepaymentModel(period.periodFromDate(), period.periodDueDate(), zeroAmount));
}
if (!interestRepaymentModelList.isEmpty()) {
interestRepaymentModelList.get(interestRepaymentModelList.size() - 1).setLastPeriod(true);
}
return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail,
installmentAmountInMultiplesOf, mc);
}
Expand Down Expand Up @@ -124,36 +127,40 @@ Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriodForIn
@Override
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
final Money disbursedAmount) {
final ProgressiveLoanInterestRepaymentModel repaymentPeriod = findInterestRepaymentPeriodForDisbursement(scheduleModel,
disbursementDueDate).orElse(null);
if (repaymentPeriod == null) {
return;
}

var interestPeriodOptional = findInterestPeriodForDisbursement(repaymentPeriod, disbursementDueDate);
if (interestPeriodOptional.isPresent()) {
interestPeriodOptional.get().addDisbursedAmount(disbursedAmount);
} else {
insertInterestPeriod(repaymentPeriod, disbursementDueDate, disbursedAmount);
}

calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel);
changeOutstandingBalanceAndUpdateInterestPeriods(scheduleModel, disbursementDueDate, disbursedAmount,
Money.zero(disbursedAmount.getCurrency()))
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel));
}

Optional<ProgressiveLoanInterestRepaymentModel> changeOutstandingBalanceAndUpdateInterestPeriods(
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate balanceChangeDate, final Money disbursedAmount,
final Money correctionAmount) {
return findInterestRepaymentPeriodForDisbursement(scheduleModel, balanceChangeDate).stream().peek(repaymentPeriod -> {
var interestPeriodOptional = findInterestPeriodForDisbursement(repaymentPeriod, balanceChangeDate);
if (interestPeriodOptional.isPresent()) {
interestPeriodOptional.get().addDisbursedAmount(disbursedAmount);
interestPeriodOptional.get().addCorrectionAmount(correctionAmount);
} else {
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount);
}
}).findFirst();
}

void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate disbursementDueDate,
final Money disbursedAmount) {
void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate balanceChangeDate,
final Money disbursedAmount, final Money correctionAmount) {
// disbursementDueDate is after disb.date because this case when disbursement date is different than interest
// period start date
final ProgressiveLoanInterestRepaymentInterestPeriod previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream()
.filter(interestPeriod -> disbursementDueDate.isAfter(interestPeriod.getFromDate())
&& disbursementDueDate.isBefore(interestPeriod.getDueDate()))//
.filter(interestPeriod -> balanceChangeDate.isAfter(interestPeriod.getFromDate())
&& balanceChangeDate.isBefore(interestPeriod.getDueDate()))//
.findFirst()//
.get();//

final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(disbursementDueDate,
previousInterestPeriod.getDueDate(), BigDecimal.ZERO, disbursedAmount, Money.zero(disbursedAmount.getCurrency()));
final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(balanceChangeDate,
previousInterestPeriod.getDueDate(), BigDecimal.ZERO, disbursedAmount, correctionAmount,
Money.zero(disbursedAmount.getCurrency()));

previousInterestPeriod.setDueDate(disbursementDueDate);
previousInterestPeriod.setDueDate(balanceChangeDate);

repaymentPeriod.getInterestPeriods().add(interestPeriod);
Collections.sort(repaymentPeriod.getInterestPeriods());
Expand Down Expand Up @@ -203,14 +210,38 @@ void insertInterestPeriod(final ProgressiveLoanInterestScheduleModel scheduleMod

final Money zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(interestChangeDueDate,
previousInterestPeriod.getDueDate(), BigDecimal.ZERO, zeroAmount, zeroAmount);
previousInterestPeriod.getDueDate(), BigDecimal.ZERO, zeroAmount, zeroAmount, zeroAmount);

previousInterestPeriod.setDueDate(interestChangeDueDate);

repaymentPeriod.getInterestPeriods().add(interestPeriod);
Collections.sort(repaymentPeriod.getInterestPeriods());
}

@Override
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount) {
final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency());
changeOutstandingBalanceAndUpdateInterestPeriods(scheduleModel, balanceCorrectionDate, zeroAmount, balanceCorrectionAmount)
.ifPresent(repaymentPeriod -> {
calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculatePrincipalInterestComponentsForPeriods(scheduleModel);
});
}

@Override
public Optional<ProgressiveLoanInterestRepaymentModel> getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel,
LocalDate date) {
final var newScheduleModel = makeScheduleModelDeepCopy(scheduleModel);
final var zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());

return changeOutstandingBalanceAndUpdateInterestPeriods(newScheduleModel, date, zeroAmount, zeroAmount).stream()
.peek(repaymentPeriod -> {
calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, date);
}).findFirst();
}

/**
* Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest
*/
Expand Down Expand Up @@ -302,11 +333,13 @@ BigDecimal calcNominalInterestRatePercentage(final BigDecimal interestRate, Math
*/
void calculateRateFactorMinus1ForPeriods(final List<ProgressiveLoanInterestRepaymentModel> repaymentPeriods,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
for (var repaymentPeriod : repaymentPeriods) {
for (var interestPeriod : repaymentPeriod.getInterestPeriods()) {
interestPeriod.setRateFactorMinus1(calculateRateFactorMinus1PerPeriod(repaymentPeriod, interestPeriod, scheduleModel));
}
}
repaymentPeriods.forEach(repaymentPeriod -> calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel));
}

void calculateRateFactorMinus1ForRepaymentPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
repaymentPeriod.getInterestPeriods().forEach(interestPeriod -> interestPeriod
.setRateFactorMinus1(calculateRateFactorMinus1PerPeriod(repaymentPeriod, interestPeriod, scheduleModel)));
}

/**
Expand Down Expand Up @@ -636,35 +669,45 @@ BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRat

void calculatePrincipalInterestComponentsForPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel) {
Money outstandingBalance = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
int numberOfPeriods = scheduleModel.repayments().size();
int period = 0;
for (var repaymentPeriod : scheduleModel.repayments()) {
final boolean isLastPeriod = ++period == numberOfPeriods;
outstandingBalance = calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, outstandingBalance, isLastPeriod);
repaymentPeriod.setOutstandingBalance(outstandingBalance);
calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, null);
outstandingBalance = repaymentPeriod.getRemainingBalance();
}
}

Money calculatePrincipalInterestComponentsForPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod,
final Money initBalance, final boolean isLastPeriod) {
Money outstandingBalance = initBalance;
void calculatePrincipalInterestComponentsForPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod,
final LocalDate calculateTill) {
final Money zeroAmount = Money.zero(repaymentPeriod.getOutstandingBalance().getCurrency());
Money outstandingBalance = repaymentPeriod.getOutstandingBalance();
Money balanceCorrection = zeroAmount;
Money cumulatedInterest = zeroAmount;

for (ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
final boolean shouldInvalidateInterestPeriod = calculateTill != null && interestPeriod.getDueDate().isAfter(calculateTill);
if (shouldInvalidateInterestPeriod) {
interestPeriod.setInterestDue(zeroAmount);
interestPeriod.setDisbursedAmount(zeroAmount);
interestPeriod.setCorrectionAmount(zeroAmount);
continue;
}
outstandingBalance = outstandingBalance.plus(interestPeriod.getDisbursedAmount());
final Money calculatedInterest = outstandingBalance.multipliedBy(interestPeriod.getRateFactorMinus1());
balanceCorrection = balanceCorrection.plus(interestPeriod.getCorrectionAmount());
final Money calculatedInterest = outstandingBalance.plus(balanceCorrection).multipliedBy(interestPeriod.getRateFactorMinus1());
interestPeriod.setInterestDue(calculatedInterest);
cumulatedInterest = cumulatedInterest.plus(calculatedInterest);
}

final Money cumulatedInterest = repaymentPeriod.getInterestDue();
final Money calculatedPrincipal = isLastPeriod ? outstandingBalance
final Money calculatedPrincipal = repaymentPeriod.isLastPeriod() ? outstandingBalance
: repaymentPeriod.getEqualMonthlyInstallment().minus(cumulatedInterest);

if (isLastPeriod) {
if (repaymentPeriod.isLastPeriod()) {
repaymentPeriod.setEqualMonthlyInstallment(calculatedPrincipal.add(cumulatedInterest));
}

final Money remainingBalance = outstandingBalance.minus(calculatedPrincipal);
repaymentPeriod.setPrincipalDue(calculatedPrincipal);
repaymentPeriod.setRemainingBalance(outstandingBalance.minus(calculatedPrincipal));

return repaymentPeriod.getRemainingBalance();
repaymentPeriod.setRemainingBalance(remainingBalance);
}

@Override
Expand All @@ -674,6 +717,10 @@ public ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDeta
.filter(period -> !period.isDownPayment() && !period.isAdditional()).toList();
List<ProgressiveLoanInterestRepaymentModel> repaymentModels = progressiveLoanInterestRepaymentModelMapper
.map(repaymentModelsWithoutDownPayment);

if (!repaymentModels.isEmpty()) {
repaymentModels.get(repaymentModels.size() - 1).setLastPeriod(true);
}
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
}
}
Loading

0 comments on commit 54d9e6b

Please sign in to comment.