Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,852 changes: 1,183 additions & 669 deletions fineract-e2e-tests-runner/src/test/resources/features/LoanReAging.feature

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,9 @@ public int compareTo(LoanRepaymentScheduleInstallment o) {
return this.installmentNumber.compareTo(o.installmentNumber);
}

public int compareToByDueDate(LoanRepaymentScheduleInstallment o) {
return this.dueDate.compareTo(o.dueDate);
public int compareToByFromDueDate(LoanRepaymentScheduleInstallment o) {
int primary = this.dueDate.compareTo(o.dueDate);
return primary == 0 ? this.fromDate.compareTo(o.fromDate) : primary;
}

public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3107,7 +3107,7 @@ private LoanRepaymentScheduleInstallment createInstallmentWithMovedPaidAmounts(f
private void reprocessInstallments(final List<LoanRepaymentScheduleInstallment> installments) {
final AtomicInteger counter = new AtomicInteger(1);
final AtomicReference<LocalDate> previousDueDate = new AtomicReference<>(null);
installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate).forEachOrdered(i -> {
installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByFromDueDate).forEachOrdered(i -> {
i.updateInstallmentNumber(counter.getAndIncrement());
final LocalDate prev = previousDueDate.get();
if (prev != null && (i.isAdditional() || i.isReAged())) {
Expand Down Expand Up @@ -3415,8 +3415,6 @@ private void handleReAgeEqualAmortizationEMICalculator(LoanTransaction loanTrans
BalancesWithPaidInAdvance paidInAdvanceBalances = liftEarlyRepaidBalances(installments, transactionDate, currency,
ctx.getAlreadyProcessedTransactions());

// TODO add as Parameter here: paidInAdvanceBalances.getAggregatedFeeChargesPortion().isGreaterThanZero() ||
// paidInAdvanceBalances.getAggregatedPenaltyChargesPortion().isGreaterThanZero()
emiCalculator.reAgeEqualAmortization(model, transactionDate, loanReAgeParameter,
outstandingBalances.fees.add(outstandingBalances.penalties),
new EqualAmortizationValues(calculatedFees.value().add(calculatedPenalties.value()),
Expand All @@ -3440,6 +3438,7 @@ private void handleReAgeEqualAmortizationEMICalculator(LoanTransaction loanTrans
installment.setInterestCharged(installment.getInterestPaid());
installment.setPrincipal(installment.getPrincipalCompleted(currency).getAmount());
installment.setInstallmentNumber(index + 1);
installment.setCreditedPrincipal(rp.getCreditedPrincipal().getAmount());

installment.updateObligationsMet(currency, transactionDate);
// TODO add remaining components
Expand Down Expand Up @@ -3471,6 +3470,7 @@ private void handleReAgeEqualAmortizationEMICalculator(LoanTransaction loanTrans

createChargeMappingsForInstallment(created, calculatedCharges, isLastRepaymentPeriod);
}
created.setCreditedPrincipal(rp.getCreditedPrincipal().getAmount());
created.updateObligationsMet(currency, transactionDate);
installments.add(created);
}
Expand All @@ -3485,6 +3485,10 @@ private void handleReAgeWithCommonStrategy(LoanTransaction loanTransaction, Comm
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
LocalDate transactionDate = loanTransaction.getTransactionDate();
LocalDate originalMaturityDate = installments.stream()
.filter(i-> !i.isDownPayment() && !i.isAdditional() && i.getDueDate()!= null)
.map(LoanRepaymentScheduleInstallment::getDueDate).max(LocalDate::compareTo)
.orElseThrow();

Integer numberOfReAgeInstallments = loanReAgeParameter.getNumberOfInstallments();
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail()
Expand Down Expand Up @@ -3549,8 +3553,7 @@ private void handleReAgeWithCommonStrategy(LoanTransaction loanTransaction, Comm
return res;
}).reduce(new BalancesWithPaidInAdvance(currency), BalancesWithPaidInAdvance::summarizerAccumulator);

if (!balances.getPrincipal().isZero() || !balances.getInterest().isZero() || !balances.getFee().isZero()
|| !balances.getPenalty().isZero()) {
if (!transactionDate.isAfter(originalMaturityDate)) {

final LoanRepaymentScheduleInstallment earlyRepaidInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
firstReAgeInstallmentProps.reAgedInstallmentNumber(), firstReAgeInstallmentProps.fromDate(), transactionDate,
Expand All @@ -3570,7 +3573,7 @@ private void handleReAgeWithCommonStrategy(LoanTransaction loanTransaction, Comm

InstallmentProcessingHelper.addOneToInstallmentNumberFromInstallment(installments,
earlyRepaidInstallment.getInstallmentNumber());
loan.getRepaymentScheduleInstallments().add(earlyRepaidInstallment);
installments.add(earlyRepaidInstallment);
}

LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,7 @@ private static void moveOutstandingAmountsFromPeriodsBeforeReAging(final List<Re
lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated());
}
rp.setEmi(rp.getTotalPaidAmount());
rp.setOutstandingMovedDueToReAging(true);
rp.moveOutstandingDueToReAging();
});
}

Expand Down Expand Up @@ -1710,40 +1710,46 @@ public void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interest
LoanReAgeParameter reageParameter, Money feesPenaltiesOutstanding,
EqualAmortizationValues feesPenaltiesEqualAmortizationValues) {
LocalDate originalMaturityDate = interestSchedule.getMaturityDate();
boolean isAfterOriginalMaturityDate = transactionDate.isAfter(originalMaturityDate);
List<RepaymentPeriod> reAgedRepaymentPeriods = new ArrayList<>(reageParameter.getNumberOfInstallments());
OutstandingDetails reAgeingAmounts = precalculateReAgeEqualAmortizationAmount(interestSchedule, transactionDate, reageParameter);
Money zero = interestSchedule.zero();

// calculate already paid balances from transaction date
OutstandingDetails paidBalancesFromTransactionDate = calculatePaidBalancesAfterDate(interestSchedule, transactionDate);

// find future chargebacks.
// find future balance corrections
Money futureCreditedPrincipals = interestSchedule.repaymentPeriods().stream()
.filter(rp -> !rp.getFromDate().isBefore(transactionDate)).filter(rp -> rp.getDueDate().isAfter(transactionDate))
.map(RepaymentPeriod::getCreditedPrincipal).reduce(zero, Money::add);

// set maturity date to transaction date and remove all repayment periods after it.
accelerateMaturityDateTo(interestSchedule, transactionDate);

addCredit(interestSchedule, transactionDate, futureCreditedPrincipals, zero);

// close all open repayment period while keep paid amounts
interestSchedule.repaymentPeriods().forEach(rp -> {
rp.getInterestPeriods().getLast()
.addCreditedInterestAmount(MathUtil.min(rp.getOutstandingInterest(), rp.getCreditedInterest(), false).negated());
rp.setEmi(rp.getTotalPaidAmount());
rp.setOutstandingMovedDueToReAging(true);
rp.moveOutstandingDueToReAging();
});

// stop calculate unrecognised interest at this point because all
interestSchedule.getLastRepaymentPeriod().setNoUnrecognisedInterest(true);

if (!paidBalancesFromTransactionDate.getOutstandingInterest().isZero()
|| !paidBalancesFromTransactionDate.getOutstandingPrincipal().isZero()) {
if (!originalMaturityDate.isBefore(transactionDate)) {
createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(interestSchedule,
paidBalancesFromTransactionDate.getOutstandingPrincipal(), paidBalancesFromTransactionDate.getOutstandingInterest(),
true);
addFirstReAgedPeriod(interestSchedule, interestSchedule.getLastRepaymentPeriod());
}

updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods, isAfterOriginalMaturityDate);
updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods);

updateEMIForReAgeEqualAmortization(reAgedRepaymentPeriods, reAgeingAmounts.getOutstandingPrincipal(),
reAgeingAmounts.getOutstandingInterest(), feesPenaltiesOutstanding, feesPenaltiesEqualAmortizationValues,
interestSchedule.zero().getCurrency());
zero.getCurrency());

calculateOutstandingBalance(interestSchedule);

Expand All @@ -1755,31 +1761,17 @@ public void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interest
}

private void updateModelForReageEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule,
LoanReAgeParameter reageParameter, List<RepaymentPeriod> reAgedRepaymentPeriods, boolean isAfterOriginalMaturityDate) {
LoanReAgeParameter reageParameter, List<RepaymentPeriod> reAgedRepaymentPeriods) {
int numberOfInstallmentsToAdd = reageParameter.getNumberOfInstallments();
LocalDate toDate = reageParameter.getStartDate();
RepaymentPeriod previous = interestSchedule.getLastRepaymentPeriod();
int frequency = reageParameter.getFrequencyNumber();
PeriodFrequencyType frequencyType = reageParameter.getFrequencyType();

if (!isAfterOriginalMaturityDate) {
// merge first reaged period
RepaymentPeriod firstReAgedPeriod = interestSchedule.getLastRepaymentPeriod();
firstReAgedPeriod.setDueDate(toDate);
firstReAgedPeriod.getLastInterestPeriod().setDueDate(toDate);
firstReAgedPeriod.setReAged(true);
firstReAgedPeriod.getPrevious().ifPresent(prev -> prev.setNoUnrecognisedInterest(true));
reAgedRepaymentPeriods.add(firstReAgedPeriod);

// update params for next reage repayment period calculation
numberOfInstallmentsToAdd--;
toDate = scheduledDateGenerator.getRepaymentPeriodDate(frequencyType, frequency, toDate);
}

// insert new reaged repayment periods
for (int i = 0; i < numberOfInstallmentsToAdd; i++) {
RepaymentPeriod repaymentPeriod = RepaymentPeriod.create(previous, previous.getDueDate(), toDate, interestSchedule.zero(),
previous.getMc(), previous.getLoanProductRelatedDetail());
interestSchedule.mc(), previous.getLoanProductRelatedDetail());
repaymentPeriod.setTotalCapitalizedIncomeAmount(previous.getTotalCapitalizedIncomeAmount());
repaymentPeriod.setTotalDisbursedAmount(previous.getTotalDisbursedAmount());
repaymentPeriod.setReAged(true);
Expand All @@ -1806,14 +1798,6 @@ private void createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(final Prog
calculateRateFactorForRepaymentPeriod(targetPeriod, interestSchedule);
}

private static void addFirstReAgedPeriod(ProgressiveLoanInterestScheduleModel interestSchedule, RepaymentPeriod targetPeriod) {
RepaymentPeriod repaymentPeriodToInsert = RepaymentPeriod.create(targetPeriod, targetPeriod.getDueDate(),
interestSchedule.getMaturityDate(), interestSchedule.zero(), interestSchedule.mc(),
interestSchedule.loanProductRelatedDetail());
repaymentPeriodToInsert.setReAged(true);
interestSchedule.repaymentPeriods().add(repaymentPeriodToInsert);
}

private OutstandingDetails calculatePaidBalancesAfterDate(ProgressiveLoanInterestScheduleModel interestSchedule,
LocalDate transactionDate) {
Money principal = interestSchedule.repaymentPeriods().stream().filter(rp -> !rp.getDueDate().isBefore(transactionDate))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ public class RepaymentPeriod {

@Getter
@Setter
private boolean isOutstandingMovedDueToReAging = false;

private Money creditedPrincipalMovedDueReAge;
@Getter
@Setter
private Money creditedInterestMovedDueReAge;
@Setter
@Getter
private boolean noUnrecognisedInterest;
Expand Down Expand Up @@ -125,6 +127,8 @@ protected RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDat
this.reAged = reAged;
this.reAgedEarlyRepaymentHolder = reAgedEarlyRepaymentHolder;
this.reAgedInterest = reAgedInterest;
this.creditedInterestMovedDueReAge = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
this.creditedInterestMovedDueReAge = Money.zero(loanProductRelatedDetail.getCurrencyData(), mc);
}

public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc, ILoanConfigurationDetails loanProductRelatedDetail) {
Expand All @@ -148,7 +152,8 @@ public static RepaymentPeriod copy(RepaymentPeriod previous, RepaymentPeriod rep
repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc,
repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(),
repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging());
newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge());
newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.copy(newRepaymentPeriod, interestPeriod, mc));
Expand All @@ -162,7 +167,8 @@ public static RepaymentPeriod copyWithoutPaidAmounts(RepaymentPeriod previous, R
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero,
zero, mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(),
repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging());
newRepaymentPeriod.setCreditedPrincipalMovedDueReAge(repaymentPeriod.getCreditedPrincipalMovedDueReAge());
newRepaymentPeriod.setCreditedInterestMovedDueReAge(repaymentPeriod.getCreditedInterestMovedDueReAge());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
var interestPeriodCopy = InterestPeriod.copy(newRepaymentPeriod, interestPeriod);
Expand Down Expand Up @@ -311,8 +317,8 @@ public Money getDuePrincipal() {
* @return
*/
public Money getTotalCreditedAmount() {
return isOutstandingMovedDueToReAging ? Money.zero(getCurrency(), getMc())
: getCreditedPrincipal().plus(getCreditedInterest(), getMc());
return getCreditedPrincipal().plus(getCreditedInterest(), getMc()).minus(getCreditedInterestMovedDueReAge(), getMc())
.minus(getCreditedPrincipalMovedDueReAge(), getMc());
}

/**
Expand Down Expand Up @@ -481,4 +487,9 @@ public Money getTotalDisbursedAmount() {
public Money getTotalCapitalizedIncomeAmount() {
return MathUtil.nullToZero(totalCapitalizedIncomeAmount, getCurrency(), getMc());
}

public void moveOutstandingDueToReAging() {
setCreditedPrincipalMovedDueReAge(getCreditedPrincipal());
setCreditedInterestMovedDueReAge(getCreditedInterest());
}
}
Loading
Loading