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 11, 2024
1 parent c9bdc2a commit 5bb5e64
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 409 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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
Expand All @@ -77,6 +78,7 @@
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
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.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
Expand Down Expand Up @@ -143,12 +145,13 @@ public Money handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbu
throw new NotImplementedException();
}

@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
// only for progressive loans
public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessProgressiveLoanTransactions(
LocalDate disbursementDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
if (loanTransactions.isEmpty()) {
return changedTransactionDetail;
return Pair.of(changedTransactionDetail, null);
}
if (charges != null) {
for (final LoanCharge loanCharge : charges) {
Expand Down Expand Up @@ -185,10 +188,18 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement
chargeOrTransaction.getLoanCharge()
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
}
List<LoanTransaction> txs = chargeOrTransactions.stream().map(ChargeOrTransaction::getLoanTransaction).filter(Optional::isPresent)
List<LoanTransaction> txs = chargeOrTransactions.stream() //
.map(ChargeOrTransaction::getLoanTransaction) //
.filter(Optional::isPresent) //
.map(Optional::get).toList();
reprocessInstallments(disbursementDate, txs, installments, currency);
return changedTransactionDetail;
return Pair.of(changedTransactionDetail, scheduleModel);
}

@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft();
}

@Override
Expand Down Expand Up @@ -281,7 +292,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 @@ -743,7 +754,7 @@ private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx
Balances balances = new Balances(zero, zero, zero, zero);
if (LoanScheduleProcessingType.HORIZONTAL
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(),
transactionAmountUnprocessed = processPeriodsHorizontally(transactionCtx, loanTransaction, transactionCtx.getCurrency(),
transactionCtx.getInstallments(), transactionCtx.getOverpaymentHolder().getMoneyObject(),
defaultPaymentAllocationRule, transactionMappings, Set.of(), balances);
} else if (LoanScheduleProcessingType.VERTICAL
Expand Down Expand Up @@ -982,6 +993,8 @@ private Money refundTransactionHorizontally(LoanTransaction loanTransaction, Mon
break outerLoop;
}
}
default -> {
}
}
}
} while (installments.stream().anyMatch(installment -> installment.getTotalPaid(currency).isGreaterThan(zero))
Expand Down Expand Up @@ -1109,7 +1122,7 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx

if (LoanScheduleProcessingType.HORIZONTAL
.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getLoanScheduleProcessingType())) {
transactionAmountUnprocessed = processPeriodsHorizontally(loanTransaction, transactionCtx.getCurrency(),
transactionAmountUnprocessed = processPeriodsHorizontally(transactionCtx, loanTransaction, transactionCtx.getCurrency(),
transactionCtx.getInstallments(), transactionAmountUnprocessed, paymentAllocationRule, transactionMappings,
transactionCtx.getCharges(), balances);
} else if (LoanScheduleProcessingType.VERTICAL
Expand All @@ -1125,7 +1138,7 @@ private void processTransaction(LoanTransaction loanTransaction, TransactionCtx
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder());
}

private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
private Money processPeriodsHorizontally(TransactionCtx transactionCtx, LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
Set<LoanCharge> charges, Balances balances) {
Expand All @@ -1134,17 +1147,21 @@ private Money processPeriodsHorizontally(LoanTransaction loanTransaction, Moneta
mapping(Function.identity(), toList())));

for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
transactionAmountUnprocessed = processAllocationsHorizontally(loanTransaction, currency, installments,
transactionAmountUnprocessed = processAllocationsHorizontally(transactionCtx, loanTransaction, currency, installments,
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(),
paymentAllocationRule.getFutureInstallmentAllocationRule(), transactionMappings, charges, balances);
}
return transactionAmountUnprocessed;
}

private Money processAllocationsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
private Money processAllocationsHorizontally(TransactionCtx transactionCtx, LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
if (transactionAmountUnprocessed.isZero()) {
return transactionAmountUnprocessed;
}

Money paidPortion;
boolean exit = false;
do {
Expand Down Expand Up @@ -1214,15 +1231,55 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo
for (LoanRepaymentScheduleInstallment inAdvanceInstallment : inAdvanceInstallments) {
Set<LoanCharge> inAdvanceInstallmentCharges = getLoanChargesOfInstallment(charges, inAdvanceInstallment,
firstNormalInstallmentNumber);
// Adjust the portion for the last installment
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment);
}

LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, inAdvanceInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY);

Loan loan = loanTransaction.getLoan();
if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing()
&& loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
LocalDate transactionDate = loanTransaction.getTransactionDate();
LocalDate payDate = inAdvanceInstallment.getFromDate().isAfter(transactionDate)
? inAdvanceInstallment.getFromDate()
: transactionDate;
ProgressiveLoanInterestRepaymentModel payableDetails = emiCalculator
.getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate).orElseThrow();

switch (paymentAllocationType) {
case IN_ADVANCE_INTEREST ->
inAdvanceInstallment.updateInterestCharged(payableDetails.getInterestDue().getAmount());
case IN_ADVANCE_PRINCIPAL ->
inAdvanceInstallment.updatePrincipal(payableDetails.getPrincipalDue().getAmount());
default -> {
}
}

paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);

switch (paymentAllocationType) {
case IN_ADVANCE_PRINCIPAL -> {
emiCalculator.addBalanceCorrection(model, payDate,
payableDetails.getOutstandingBalance().multipliedBy(-1));
emiCalculator.addBalanceCorrection(model, payDate,
payableDetails.getPrincipalDue().minus(paidPortion));
}
case IN_ADVANCE_INTEREST -> emiCalculator.addBalanceCorrection(model, payDate,
payableDetails.getInterestDue().minus(paidPortion));
default -> {
}
}
} else {
// Adjust the portion for the last installment
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment);
}
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
evenPortion, loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges, balances,
LoanRepaymentScheduleInstallment.PaymentAction.PAY);
}
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
}
} else {
Expand Down
Loading

0 comments on commit 5bb5e64

Please sign in to comment.