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 Aug 28, 2024
1 parent 6ca0268 commit 2acf985
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 54 deletions.
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 @@ -169,18 +171,18 @@ public LoanRepaymentScheduleInstallment() {
}

public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate,
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
final BigDecimal penaltyCharges, final boolean recalculatedInterestComponent,
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails, final BigDecimal rescheduleInterestPortion) {
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
final BigDecimal penaltyCharges, final boolean recalculatedInterestComponent,
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails, final BigDecimal rescheduleInterestPortion) {
this(loan, installmentNumber, fromDate, dueDate, principal, interest, feeCharges, penaltyCharges, recalculatedInterestComponent,
compoundingDetails, rescheduleInterestPortion, false);
}

public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate,
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
final BigDecimal penaltyCharges, final boolean recalculatedInterestComponent,
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails, final BigDecimal rescheduleInterestPortion,
final boolean isDownPayment) {
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
final BigDecimal penaltyCharges, final boolean recalculatedInterestComponent,
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails, final BigDecimal rescheduleInterestPortion,
final boolean isDownPayment) {
this.loan = loan;
this.installmentNumber = installmentNumber;
this.fromDate = fromDate;
Expand All @@ -200,9 +202,9 @@ public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installme
}

public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate,
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
final BigDecimal penaltyCharges, final boolean recalculatedInterestComponent,
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails) {
final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
final BigDecimal penaltyCharges, final boolean recalculatedInterestComponent,
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails) {
this.loan = loan;
this.installmentNumber = installmentNumber;
this.fromDate = fromDate;
Expand All @@ -228,9 +230,9 @@ public LoanRepaymentScheduleInstallment(final Loan loan) {
}

public LoanRepaymentScheduleInstallment(Loan loan, Integer installmentNumber, LocalDate fromDate, LocalDate dueDate,
BigDecimal principal, BigDecimal interestCharged, BigDecimal feeChargesCharged, BigDecimal penaltyCharges,
BigDecimal creditedPrincipal, BigDecimal creditedFee, BigDecimal creditedPenalty, boolean additional, boolean isDownPayment,
boolean isReAged) {
BigDecimal principal, BigDecimal interestCharged, BigDecimal feeChargesCharged, BigDecimal penaltyCharges,
BigDecimal creditedPrincipal, BigDecimal creditedFee, BigDecimal creditedPenalty, boolean additional, boolean isDownPayment,
boolean isReAged) {
this.loan = loan;
this.installmentNumber = installmentNumber;
this.fromDate = fromDate;
Expand All @@ -248,7 +250,7 @@ public LoanRepaymentScheduleInstallment(Loan loan, Integer installmentNumber, Lo
}

public static LoanRepaymentScheduleInstallment newReAgedInstallment(final Loan loan, final Integer installmentNumber,
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal) {
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal) {
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal, null, null, null, null, null,
null, false, false, true);
}
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 Expand Up @@ -717,7 +723,7 @@ public boolean isOverdueOn(final LocalDate date) {
}

public void updateChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff,
final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) {
final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) {
this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.getAmount());
this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.getAmount());
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.getAmount());
Expand All @@ -727,7 +733,7 @@ public void updateChargePortion(final Money feeChargesDue, final Money feeCharge
}

public void addToChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff,
final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) {
final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) {
this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.plus(this.feeChargesCharged).getAmount());
this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.plus(this.feeChargesWaived).getAmount());
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.plus(this.feeChargesWrittenOff).getAmount());
Expand All @@ -754,7 +760,7 @@ public void updateObligationsMet(final MonetaryCurrency currency, final LocalDat
}

private void trackAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency,
final Money amountPaidInRepaymentPeriod) {
final Money amountPaidInRepaymentPeriod) {
if (isInAdvance(transactionDate)) {
this.totalPaidInAdvance = asMoney(this.totalPaidInAdvance, currency).plus(amountPaidInRepaymentPeriod).getAmount();
} else if (isLatePayment(transactionDate)) {
Expand Down Expand Up @@ -976,7 +982,7 @@ public Money unpayPrincipalComponent(final LocalDate transactionDate, final Mone
}

private void reduceAdvanceAndLateTotalsForRepaymentPeriod(final LocalDate transactionDate, final MonetaryCurrency currency,
final Money amountDeductedInRepaymentPeriod) {
final Money amountDeductedInRepaymentPeriod) {

if (isInAdvance(transactionDate)) {
Money mTotalPaidInAdvance = Money.of(currency, this.totalPaidInAdvance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ProgressiveLoanScheduleGenerator;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
Expand Down Expand Up @@ -281,7 +282,7 @@ protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransactio
}
return originalTransaction.get();
} else { // when there is no id, then it might be that the original transaction is changed, so we need to look
// it up from the Ctx.
// it up from the Ctx.
Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction);
Collection<LoanTransaction> updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values();
Optional<LoanTransaction> updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations()
Expand Down Expand Up @@ -1157,22 +1158,22 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo

// For having similar logic we are populating installment list even when the future installment
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = new ArrayList<>();
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
List<LoanRepaymentScheduleInstallment> inAdvanceInstallments = switch (futureInstallmentAllocationRule) {
case REAMORTIZATION -> installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate())).toList();
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
}
case NEXT_INSTALLMENT -> // first future unpaid installment
installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
case LAST_INSTALLMENT -> // last future unpaid installment
installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
};

int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);

Money originalInterestOfCurrentInstallment = null;
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> {
Expand Down Expand Up @@ -1206,6 +1207,51 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo
case IN_ADVANCE -> {
int numberOfInstallments = inAdvanceInstallments.size();
if (numberOfInstallments > 0) {
if (loanTransaction.getLoan().getLoanProduct().isInterestRecalculationEnabled()) {
AtomicReference<Money> sumAdjusted = new AtomicReference<>(Money.zero(currency));

// recalculate interest before processing payment
for (LoanRepaymentScheduleInstallment installment : inAdvanceInstallments) {
LocalDate transactionDate = loanTransaction.getTransactionDate();
if (installment.isCurrentInstallment(transactionDate)) {
switch (paymentAllocationType) {
case IN_ADVANCE_INTEREST -> {
Money payableInterest = ProgressiveLoanScheduleGenerator
.calculatePayableInterest(installment, transactionDate);
originalInterestOfCurrentInstallment = installment.getInterestCharged(currency);
installment.updateInterestCharged(payableInterest.getAmount());
}
case IN_ADVANCE_PRINCIPAL -> {
Money interestDelta = calculateInterestDelta(originalInterestOfCurrentInstallment,
installment, transactionDate, currency);
sumAdjusted.updateAndGet(v -> v.add(interestDelta));
BigDecimal newPrincipal = installment.getPrincipal(currency).plus(interestDelta)
.getAmount();
installment.updatePrincipal(newPrincipal);
}
}

// find last installment number
int lastInstallmentNumber = installments.stream() //
.mapToInt(LoanRepaymentScheduleInstallment::getInstallmentNumber) //
.max().orElse(0);

// update later installments with zero interest and increased principal
installments.stream().filter(it -> it.getInstallmentNumber() > installment.getInstallmentNumber())
.forEach(it -> {
Money interestCharged = it.getInterestCharged(currency);
sumAdjusted.updateAndGet(v -> v.add(interestCharged));
it.updateInterestCharged(BigDecimal.ZERO);
BigDecimal newPrincipal = it.getPrincipal(currency).plus(interestCharged).getAmount();
if (it.getInstallmentNumber() == lastInstallmentNumber) {
newPrincipal = newPrincipal.subtract(sumAdjusted.get().getAmount());
}
it.updatePrincipal(newPrincipal);
});
}
}
}

// This will be the same amount as transactionAmountUnprocessed in case of the future
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
Expand Down Expand Up @@ -1239,6 +1285,19 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo
return transactionAmountUnprocessed;
}

private Money calculateInterestDelta(Money originalInterestOfCurrentInstallment, LoanRepaymentScheduleInstallment installment,
LocalDate transactionDate, MonetaryCurrency currency) {
if (originalInterestOfCurrentInstallment == null) {
// interest was not recalculated yet
Money payableInterest = ProgressiveLoanScheduleGenerator.calculatePayableInterest(installment, transactionDate);
originalInterestOfCurrentInstallment = installment.getInterestCharged(currency);
return originalInterestOfCurrentInstallment.minus(payableInterest);
} else {
// interest was already recalculated
return originalInterestOfCurrentInstallment.minus(installment.getInterestCharged(currency));
}
}

@NotNull
private static Set<LoanCharge> getLoanChargesOfInstallment(Set<LoanCharge> charges, LoanRepaymentScheduleInstallment currentInstallment,
int firstNormalInstallmentNumber) {
Expand Down
Loading

0 comments on commit 2acf985

Please sign in to comment.