Skip to content
Draft
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1824,4 +1824,7 @@ public boolean hasContractTerminationTransaction() {
return getLoanTransactions().stream().anyMatch(t -> t.isContractTermination() && t.isNotReversed());
}

public boolean hasReAgingTransaction() {
return getLoanTransactions().stream().anyMatch(t -> t.isReAge() && t.isNotReversed());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ public void handleRepaymentOrRecoveryOrWaiverTransaction(final Loan loan, final
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
reprocessLoanTransactionsService.reprocessTransactions(loan);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@
public class EmbeddableProgressiveLoanScheduleGenerator {

private final ProgressiveLoanScheduleGenerator scheduleGenerator;
private final ScheduledDateGenerator scheduledDateGenerator;
private final EMICalculator emiCalculator;

public EmbeddableProgressiveLoanScheduleGenerator() {
this.emiCalculator = new ProgressiveEMICalculator();
this.scheduledDateGenerator = new DefaultScheduledDateGenerator();
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
final EMICalculator emiCalculator = new ProgressiveEMICalculator(scheduledDateGenerator);
this.scheduleGenerator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
new NoopInterestScheduleModelRepositoryWrapper());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public BigDecimal calculateInterestRecalculationFutureOutstandingValue(Loan loan
ctx.setChargedOff(loan.isChargedOff());
ctx.setWrittenOff(loan.isClosedWrittenOff());
ctx.setContractTerminated(loan.isContractTermination());
ctx.setReAged(loan.hasReAgingTransaction());
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(nextPaymentDueDate, ctx, false);
RepaymentPeriod repaymentPeriod = scheduleModel.findRepaymentPeriodByDueDate(nextPaymentDueDate)
.orElseGet(scheduleModel::getLastRepaymentPeriod);
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public class ProgressiveTransactionCtx extends TransactionCtx {
@Setter
private boolean isContractTerminated = false;
@Setter
private boolean isReAged = false;
@Setter
private boolean isPrepayAttempt = false;
private final List<LoanRepaymentScheduleInstallment> skipRepaymentScheduleInstallments = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public Optional<ProgressiveLoanInterestScheduleModel> getSavedModel(Loan loan, L
ctx.setChargedOff(loan.isChargedOff());
ctx.setWrittenOff(loan.isClosedWrittenOff());
ctx.setContractTerminated(loan.isContractTermination());
ctx.setReAged(loan.hasReAgingTransaction());
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(businessDate, ctx);
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
Expand Down Expand Up @@ -140,4 +142,7 @@ Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel sc
* interest paused.
*/
void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate);

void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel scheduleModel, LoanTransaction loanTransaction,
LoanApplicationTerms loanApplicationTerms, MathContext mc);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,21 @@
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
Expand All @@ -66,6 +72,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
private static final BigDecimal DIVISOR_100 = new BigDecimal("100");
private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);

private final ScheduledDateGenerator scheduledDateGenerator;

@Override
@NotNull
public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
Expand Down Expand Up @@ -559,6 +567,125 @@ private void calculateEMIValueAndRateFactorsForDecliningBalanceInterestMethod(fi
}
}

@Override
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LoanTransaction loanTransaction, final LoanApplicationTerms loanApplicationTerms, final MathContext mc) {
final MonetaryCurrency currency = loanTransaction.getLoan().loanCurrency();
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();

final List<RepaymentPeriod> periodsBeforeReAging = existingRepaymentPeriods.stream()
.filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList();

final List<RepaymentPeriod> periodsWhichGotReAgedAndHavePaidAmount = existingRepaymentPeriods.stream()
.filter(rp -> !rp.getDueDate().isBefore(reAgingStartDate) && rp.getTotalPaidAmount().isGreaterThanZero()).toList();

final AtomicReference<Money> totalPaidPrincipalFromReAgedPeriods = new AtomicReference<>(Money.zero(currency));
final AtomicReference<Money> totalPaidInterestFromReAgedPeriods = new AtomicReference<>(Money.zero(currency));
periodsWhichGotReAgedAndHavePaidAmount.forEach(rp -> {
totalPaidPrincipalFromReAgedPeriods.set(totalPaidPrincipalFromReAgedPeriods.get().add(rp.getPaidPrincipal()));
totalPaidInterestFromReAgedPeriods.set(totalPaidInterestFromReAgedPeriods.get().add(rp.getPaidInterest()));
rp.addPaidPrincipalAmount(rp.getPaidPrincipal().negated());
rp.addPaidInterestAmount(rp.getPaidInterest().negated());
});

RepaymentPeriod repaymentPeriodWithMovedPaidAmount = null;
if (totalPaidPrincipalFromReAgedPeriods.get().isGreaterThanZero(mc)
|| totalPaidInterestFromReAgedPeriods.get().isGreaterThanZero(mc)) {
final int indexOfLastPeriodBeforeReAging = existingRepaymentPeriods.indexOf(periodsBeforeReAging.getLast());
repaymentPeriodWithMovedPaidAmount = RepaymentPeriod.create(
periodsBeforeReAging.getLast().getPrevious().isPresent() ? periodsBeforeReAging.getLast().getPrevious().get() : null,
loanTransaction.getTransactionDate(), loanTransaction.getTransactionDate(),
totalPaidPrincipalFromReAgedPeriods.get().add(totalPaidInterestFromReAgedPeriods.get()), MoneyHelper.getMathContext(),
loanTransaction.getLoan().getLoanProductRelatedDetail());
repaymentPeriodWithMovedPaidAmount
.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount());
repaymentPeriodWithMovedPaidAmount.addPaidPrincipalAmount(totalPaidPrincipalFromReAgedPeriods.get());
repaymentPeriodWithMovedPaidAmount.addPaidInterestAmount(totalPaidInterestFromReAgedPeriods.get());
existingRepaymentPeriods.add(indexOfLastPeriodBeforeReAging, repaymentPeriodWithMovedPaidAmount);
}

periodsBeforeReAging.forEach(rp -> {
final InterestPeriod lastInterestPeriod = rp.getInterestPeriods().getLast();
lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated());
rp.setEmi(rp.getTotalPaidAmount());
});

final LocalDate periodStartDate = switch (loanReAgeParameter.getFrequencyType()) {
case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber());
case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber());
case MONTHS -> reAgingStartDate.minusMonths(loanReAgeParameter.getFrequencyNumber());
case YEARS -> reAgingStartDate.minusYears(loanReAgeParameter.getFrequencyNumber());
case WHOLE_TERM -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WHOLE_TERM");
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
};

LocalDate disbursementDate = transactionDate;
if (!reAgingStartDate.isAfter(transactionDate)) {
disbursementDate = periodStartDate;
}

// generate list of proposed schedule due dates
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
periodStartDate, loanApplicationTerms, null);
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), null,
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);

addDisbursement(temporaryReAgedScheduleModel, EmiChangeOperation.disburse(disbursementDate, loanApplicationTerms.getPrincipal()));

final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods();

if (newPeriods.isEmpty()) {
return;
}

final LocalDate reAgeDate = newPeriods.getFirst().getDueDate();
final Optional<RepaymentPeriod> firstExistingRepaymentPeriodOpt = existingRepaymentPeriods.stream()
.filter(period -> period.getDueDate().equals(reAgeDate)).findFirst();

for (final RepaymentPeriod newPeriod : newPeriods) {
final Optional<RepaymentPeriod> existingRepaymentPeriodOpt = existingRepaymentPeriods.stream().filter(
period -> period.getFromDate().equals(newPeriod.getFromDate()) && period.getDueDate().equals(newPeriod.getDueDate()))
.findFirst();
Optional<RepaymentPeriod> previousExistingRepaymentPeriodOpt = Optional.empty();
if (existingRepaymentPeriodOpt.isPresent() && firstExistingRepaymentPeriodOpt.isPresent()
&& existingRepaymentPeriodOpt.get().equals(firstExistingRepaymentPeriodOpt.get())) {
if (repaymentPeriodWithMovedPaidAmount == null) {
previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt.get().getPrevious();
} else {
previousExistingRepaymentPeriodOpt = Optional.of(repaymentPeriodWithMovedPaidAmount);
}
}

final Money newPrincipal = newPeriod.getDuePrincipal();
final Money newInterest = newPeriod.getDueInterest();

final RepaymentPeriod rp = RepaymentPeriod.create(
previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(),
newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(),
loanTransaction.getLoan().getLoanProductRelatedDetail());
rp.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount());

existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove);
existingRepaymentPeriods.add(rp);
calculateRateFactorForRepaymentPeriod(rp, scheduleModel);
}

final RepaymentPeriod lastReAgedInstallment = newPeriods.getLast();
final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream()
.filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate)
|| repaymentPeriod.getDueDate().isEqual(reAgingStartDate))
&& !repaymentPeriod.getDueDate().isAfter(lastReAgedInstallment.getDueDate()))
.toList();

calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, transactionDate);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods);
}

private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate tillDate) {
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.reduce((first, second) -> second);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
@ExtendWith(MockitoExtension.class)
class LoanScheduleGeneratorTest {

private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class));
private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$");
private static final CurrencyData CURRENCY = APPLICATION_CURRENCY.toData();
private static final BigDecimal DISBURSEMENT_AMOUNT = BigDecimal.valueOf(192.22);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
package org.apache.fineract.portfolio.loanproduct.calc;

import static org.mockito.Mockito.mock;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
Expand All @@ -39,6 +41,7 @@
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestScheduleModelParserServiceGsonImpl;
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
Expand All @@ -64,7 +67,7 @@
@ExtendWith(MockitoExtension.class)
class ProgressiveEMICalculatorTest {

private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class));

private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,8 @@ interestRefundTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaym
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
loan.getLoanTransactions().add(refundTransaction);
Expand Down
Loading
Loading