Skip to content

Commit

Permalink
FINERACT-1981: pay off schedule handling
Browse files Browse the repository at this point in the history
  • Loading branch information
kjozsa committed Sep 9, 2024
1 parent d7654dc commit 547b22f
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,24 @@ public Money getTotalOutstanding() {
.plus(penaltyCharges());
}

public void plusPrincipal(Money principal) {
public OutstandingAmountsDTO plusPrincipal(Money principal) {
this.principal = this.principal.plus(principal);
return this;
}

public void plusInterest(Money interest) {
public OutstandingAmountsDTO plusInterest(Money interest) {
this.interest = this.interest.plus(interest);
return this;
}

public void plusFeeCharges(Money feeCharges) {
public OutstandingAmountsDTO plusFeeCharges(Money feeCharges) {
this.feeCharges = this.feeCharges.plus(feeCharges);
return this;
}

public void plusPenaltyCharges(Money penaltyCharges) {
public OutstandingAmountsDTO plusPenaltyCharges(Money penaltyCharges) {
this.penaltyCharges = this.penaltyCharges.plus(penaltyCharges);
return this;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;

import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down Expand Up @@ -488,6 +490,10 @@ public void resetChargesCharged() {
this.penaltyCharges = null;
}

public boolean isCurrentInstallment(LocalDate transactionDate) {
return getFromDate().isBefore(transactionDate) && !getDueDate().isBefore(transactionDate);
}

public interface PaymentFunction {

Money accept(LocalDate transactionDate, Money transactionAmountRemaining);
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;

import static java.time.temporal.ChronoUnit.DAYS;
import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE;

import java.math.BigDecimal;
import java.math.MathContext;
Expand All @@ -29,8 +30,8 @@
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.MathUtil;
Expand All @@ -46,6 +47,7 @@
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
Expand Down Expand Up @@ -247,58 +249,44 @@ public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicatio
public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
return switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
case TILL_PRE_CLOSURE_DATE -> {
log.debug("calculating prepayment amount till pre closure date (Strategy A)");
OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency);
AtomicBoolean firstAfterPayoff = new AtomicBoolean(true);
loan.getRepaymentScheduleInstallments().forEach(installment -> {
boolean isInstallmentAfterPayoff = installment.getDueDate().isAfter(onDate);

outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency));
if (isInstallmentAfterPayoff) {
if (firstAfterPayoff.getAndSet(false)) {
outstandingAmounts.plusInterest(calculatePayableInterest(loan, installment, onDate));
} else {
log.debug("Installment {} - {} is after payoff, not counting interest", installment.getFromDate(),
installment.getDueDate());
}
} else {
log.debug("adding interest for {} - {}: {}", installment.getFromDate(), installment.getDueDate(),
installment.getInterestOutstanding(currency));
outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency));
}
outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency));
outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency));
});
yield outstandingAmounts;
}

case TILL_REST_FREQUENCY_DATE -> {
log.debug("calculating prepayment amount till rest frequency date (Strategy B)");
OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency);
loan.getRepaymentScheduleInstallments().forEach(installment -> {
boolean isPayoffBeforeInstallment = installment.getFromDate().isBefore(onDate);

outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency));
if (isPayoffBeforeInstallment) {
outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency));
} else {
log.debug("Payoff after installment {}, not counting interest", installment.getDueDate());
}
outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency));
outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency));
});

yield outstandingAmounts;
}
case NONE -> throw new UnsupportedOperationException("Pre-closure interest calculation strategy not supported");
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();

LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
case TILL_PRE_CLOSURE_DATE -> onDate;
case TILL_REST_FREQUENCY_DATE -> // find due date of current installment
installments.stream().filter(it -> it.getFromDate().isBefore(onDate) && it.getDueDate().isAfter(onDate)).findFirst()
.orElseThrow(() -> new IllegalStateException("No installment found for transaction date: " + onDate)).getDueDate();
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
};

if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
}
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(),
loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight();

LoanRepaymentScheduleInstallment actualInstallment = installments.stream()
.filter(it -> transactionDate.isAfter(it.getFromDate()) && !transactionDate.isAfter(it.getDueDate()))
.findFirst()
.orElse(installments.get(0));

ProgressiveLoanInterestRepaymentModel result = emiCalculator.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate).orElseThrow();

OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) //
.principal(result.getOutstandingBalance()) //
.interest(result.getInterestDue());

installments.forEach(installment -> amounts //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));

return amounts;
}

private Money calculatePayableInterest(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate onDate) {
@Deprecated
public static Money calculatePayableInterest(LoanRepaymentScheduleInstallment installment, LocalDate onDate) {
RoundingMode roundingMode = MoneyHelper.getRoundingMode();
MonetaryCurrency currency = loan.getCurrency();
MonetaryCurrency currency = installment.getLoan().getCurrency();
Money originalInterest = installment.getInterestCharged(currency);
log.debug("calculating interest for {} from {} to {}", originalInterest, installment.getFromDate(), installment.getDueDate());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PrincipalInterest;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;

public interface EMICalculator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,14 @@ public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleM
Optional<ProgressiveLoanInterestRepaymentModel> changeOutstandingBalanceAndUpdateInterestPeriods(
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate balanceChangeDate, final Money disbursedAmount,
final Money correctionAmount) {
return findInterestRepaymentPeriodForBalanceChange(scheduleModel, balanceChangeDate)
.stream()//
return findInterestRepaymentPeriodForBalanceChange(scheduleModel, balanceChangeDate).stream()//
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount))//
.findFirst();//
}

@NotNull
private Consumer<ProgressiveLoanInterestRepaymentModel> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount, final Money correctionAmount) {
private Consumer<ProgressiveLoanInterestRepaymentModel> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate,
final Money disbursedAmount, final Money correctionAmount) {
return repaymentPeriod -> {
var interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate);
if (interestPeriodOptional.isPresent()) {
Expand Down Expand Up @@ -182,8 +182,8 @@ void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentP
private static @NotNull Predicate<ProgressiveLoanInterestRepaymentInterestPeriod> operationRelatedPreviousInterestPeriod(
ProgressiveLoanInterestRepaymentModel repaymentPeriod, LocalDate operationDate) {
return interestPeriod -> operationDate.isAfter(interestPeriod.getFromDate())
&& (operationDate.isBefore(interestPeriod.getDueDate())
|| (repaymentPeriod.getDueDate().equals(interestPeriod.getDueDate()) && !operationDate.isBefore(repaymentPeriod.getDueDate())));
&& (operationDate.isBefore(interestPeriod.getDueDate()) || (repaymentPeriod.getDueDate().equals(interestPeriod.getDueDate())
&& !operationDate.isBefore(repaymentPeriod.getDueDate())));
}

@Override
Expand Down Expand Up @@ -250,12 +250,12 @@ public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleMo
}

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

return findInterestRepaymentPeriod(newScheduleModel, periodDueDate)
.stream()
return findInterestRepaymentPeriod(newScheduleModel, periodDueDate).stream()
.peek(updateInterestPeriodOnRepaymentPeriod(payDate, zeroAmount, zeroAmount))//
.peek(repaymentPeriod -> {
calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public void calculatePrepaymentAmount_TILL_PRE_CLOSURE_DATE() {

OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32,
loan, holidays, processor);
assertEquals(BigDecimal.valueOf(83.84), amounts.getTotalOutstanding().getAmount());
assertEquals(BigDecimal.valueOf(83.81), amounts.getTotalOutstanding().getAmount());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay
Assertions.assertEquals(0.0, toDouble(repaymentDetails.getInterestDue().getAmount()));

// schedule 2nd period last day
repaymentDetails = emiCalculator
.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)).get();
repaymentDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)).get();
Assertions.assertEquals(83.57, toDouble(repaymentDetails.getOutstandingBalance().getAmount()));
Assertions.assertEquals(16.52, toDouble(repaymentDetails.getPrincipalDue().getAmount()));
Assertions.assertEquals(0.49, toDouble(repaymentDetails.getInterestDue().getAmount()));
Expand Down Expand Up @@ -502,10 +501,14 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay
Assertions.assertEquals(16.77, toDouble(details.getPrincipalDue().getAmount()));
Assertions.assertEquals(0.24, toDouble(details.getInterestDue().getAmount()));

emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 3, 1), Money.of(monetaryCurrency, BigDecimal.valueOf(-66.80)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 1), Money.of(monetaryCurrency, BigDecimal.valueOf(-49.79)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 5, 1), Money.of(monetaryCurrency, BigDecimal.valueOf(-32.78)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 1), Money.of(monetaryCurrency, BigDecimal.valueOf(-15.77)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 3, 1),
Money.of(monetaryCurrency, BigDecimal.valueOf(-66.80)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 1),
Money.of(monetaryCurrency, BigDecimal.valueOf(-49.79)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 5, 1),
Money.of(monetaryCurrency, BigDecimal.valueOf(-32.78)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 1),
Money.of(monetaryCurrency, BigDecimal.valueOf(-15.77)));

details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1)).get();
Assertions.assertEquals(15.77, toDouble(details.getOutstandingBalance().getAmount()));
Expand Down

0 comments on commit 547b22f

Please sign in to comment.