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 10, 2024
1 parent c9bdc2a commit 5edacac
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 424 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 @@ -30,7 +31,6 @@
import java.util.HashSet;
import java.util.List;
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 +46,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,77 +248,38 @@ 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");
};
}

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

LocalDate start = installment.getFromDate();
Money payableInterest = Money.zero(currency);

while (!start.isEqual(onDate)) {
long between = DAYS.between(start, installment.getDueDate());
Money dailyInterest = originalInterest.minus(payableInterest).dividedBy(between, roundingMode);
log.debug("Daily interest is {}: {} / {}, total: {}", dailyInterest, originalInterest.minus(payableInterest), between,
payableInterest.add(dailyInterest));
payableInterest = payableInterest.add(dailyInterest);
start = start.plusDays(1);
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());

payableInterest = payableInterest.minus(installment.getInterestPaid(currency).minus(installment.getInterestWaived(currency)));
installments.forEach(installment -> amounts //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));

log.debug("Payable interest is {}", payableInterest);
return payableInterest;
return amounts;
}

// Private, internal methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, Loca
void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount);

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

ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel);

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
Loading

0 comments on commit 5edacac

Please sign in to comment.