Skip to content

Commit c50ed19

Browse files
oleksii-novikov-onixadamsaghy
authored andcommitted
FINERACT-1981: Fix reschedule installment for progressive loans
1 parent 9e32b37 commit c50ed19

File tree

10 files changed

+595
-219
lines changed

10 files changed

+595
-219
lines changed

fineract-e2e-tests-runner/src/test/resources/features/LoanReschedule.feature

Lines changed: 281 additions & 22 deletions
Large diffs are not rendered by default.

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ public List<LoanScheduleModelRepaymentPeriod> generateRepaymentPeriods(final Mat
7676

7777
private LocalDate applyLoanTermVariations(final LoanApplicationTerms loanApplicationTerms, final LocalDate scheduledDueDate) {
7878
LocalDate modifiedScheduledDueDate = scheduledDueDate;
79+
80+
if (LoanScheduleType.PROGRESSIVE.equals(loanApplicationTerms.getLoanScheduleType())) {
81+
return modifiedScheduledDueDate;
82+
}
83+
7984
// due date changes should be applied only for that dueDate
8085
if (loanApplicationTerms.getLoanTermVariations() != null) {
8186
if (loanApplicationTerms.getLoanTermVariations().hasDueDateVariation(scheduledDueDate)) {

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import java.util.function.Function;
5656
import java.util.function.Predicate;
5757
import java.util.stream.Collectors;
58+
import java.util.stream.IntStream;
5859
import java.util.stream.Stream;
5960
import lombok.AllArgsConstructor;
6061
import lombok.Getter;
@@ -225,8 +226,7 @@ public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> repr
225226
.collect(Collectors.toCollection(ArrayList::new));
226227
final Integer installmentAmountInMultiplesOf = loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf();
227228
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
228-
LoanConfigurationDetailsMapper.map(loan), loanTermVariations, installmentAmountInMultiplesOf,
229-
overpaymentHolder.getMoneyObject().getMc());
229+
LoanConfigurationDetailsMapper.map(loan), installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
230230
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
231231
changedTransactionDetail, scheduleModel);
232232

@@ -236,8 +236,8 @@ public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> repr
236236
List<LoanTransaction> overpaidTransactions = new ArrayList<>();
237237
for (final ChangeOperation changeOperation : changeOperations) {
238238
if (changeOperation.isLoanTermVariationsData()) {
239-
final LoanTermVariationsData interestRateChange = changeOperation.getLoanTermVariationsData().get();
240-
processLoanTermVariation(installments, interestRateChange, scheduleModel);
239+
final LoanTermVariationsData termVariationsData = changeOperation.getLoanTermVariationsData().get();
240+
processLoanTermVariation(installments, termVariationsData, scheduleModel);
241241
} else if (changeOperation.isTransaction()) {
242242
LoanTransaction transaction = changeOperation.getLoanTransaction().get();
243243
if (loan.getStatus().isOverpaid() && transaction.isAccrualActivity()) {
@@ -309,10 +309,92 @@ private void processLoanTermVariation(final List<LoanRepaymentScheduleInstallmen
309309
case INTEREST_PAUSE -> handleInterestPause(installments, termVariationsData, scheduleModel);
310310
case INTEREST_RATE_FROM_INSTALLMENT -> handleChangeInterestRate(installments, termVariationsData, scheduleModel);
311311
case EXTEND_REPAYMENT_PERIOD -> handleExtraRepaymentPeriod(installments, termVariationsData, scheduleModel);
312+
case DUE_DATE -> handleDueDateChangeOnRepaymentPeriod(installments, termVariationsData, scheduleModel);
312313
default -> throw new IllegalStateException("Unhandled LoanTermVariationType.");
313314
}
314315
}
315316

317+
private void handleDueDateChangeOnRepaymentPeriod(final List<LoanRepaymentScheduleInstallment> installments,
318+
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
319+
final LocalDate targetRepaymentPeriodDueDate = termVariationsData.getTermVariationApplicableFrom();
320+
final LocalDate newDueDate = termVariationsData.getDateValue();
321+
final Loan loan = installments.getFirst().getLoan();
322+
final LoanApplicationTerms loanApplicationTerms = new LoanApplicationTerms.Builder() //
323+
.currency(loan.getCurrency().toData()) //
324+
.repaymentEvery(loan.getLoanProductRelatedDetail().getRepayEvery()) //
325+
.repaymentPeriodFrequencyType(loan.getLoanProductRelatedDetail().getRepaymentPeriodFrequencyType()) //
326+
.fixedLength(loan.getLoanProductRelatedDetail().getFixedLength()) //
327+
.seedDate(newDueDate) //
328+
.build();
329+
emiCalculator.changeDueDate(scheduleModel, loanApplicationTerms, targetRepaymentPeriodDueDate, newDueDate);
330+
331+
IntStream.range(0, installments.size()).filter(i -> installments.get(i).getDueDate().equals(targetRepaymentPeriodDueDate))
332+
.findFirst().ifPresent(targetInstallmentIndex -> {
333+
long scheduleModelStartIndex = installments.subList(0, targetInstallmentIndex).stream()
334+
.filter(inst -> !inst.isDownPayment() && !inst.isAdditional()).count();
335+
336+
for (int i = targetInstallmentIndex; i < installments.size(); i++) {
337+
final LoanRepaymentScheduleInstallment installment = installments.get(i);
338+
if (installment.isDownPayment() || installment.isAdditional()) {
339+
continue;
340+
}
341+
if (scheduleModelStartIndex >= scheduleModel.repaymentPeriods().size()) {
342+
break;
343+
}
344+
345+
final RepaymentPeriod repaymentPeriod = scheduleModel.repaymentPeriods().get((int) scheduleModelStartIndex);
346+
347+
if (isNotObligationsMet(installment)) {
348+
installment.updateFromDate(repaymentPeriod.getFromDate());
349+
installment.updateDueDate(repaymentPeriod.getDueDate());
350+
installment.updatePrincipal(repaymentPeriod.getDuePrincipal().getAmount());
351+
installment.updateInterestCharged(repaymentPeriod.getDueInterest().getAmount());
352+
}
353+
354+
scheduleModelStartIndex++;
355+
}
356+
});
357+
358+
mergeAdditionalInstallmentsBeforeMaturityDate(installments, scheduleModel, loan);
359+
360+
installments.sort(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate));
361+
int installmentNumber = 1;
362+
for (LoanRepaymentScheduleInstallment installment : installments) {
363+
installment.updateInstallmentNumber(installmentNumber++);
364+
}
365+
}
366+
367+
private void mergeAdditionalInstallmentsBeforeMaturityDate(final List<LoanRepaymentScheduleInstallment> installments,
368+
final ProgressiveLoanInterestScheduleModel scheduleModel, final Loan loan) {
369+
final LocalDate newMaturityDate = scheduleModel.repaymentPeriods().getLast().getDueDate();
370+
371+
final Optional<LoanRepaymentScheduleInstallment> lastRegularInstallmentOptional = installments.stream() //
372+
.filter(i -> !i.isDownPayment() && !i.isAdditional() && !i.isReAged()) //
373+
.reduce((first, second) -> second);
374+
375+
lastRegularInstallmentOptional.ifPresent(lastRegularInstallment -> {
376+
final MonetaryCurrency currency = loan.getCurrency();
377+
installments.stream() //
378+
.filter(i -> i.isAdditional() && i.getDueDate() != null && i.getDueDate().isBefore(newMaturityDate))
379+
.forEach(additionalInstallment -> {
380+
final Money mergedFees = lastRegularInstallment.getFeeChargesCharged(currency)
381+
.plus(additionalInstallment.getFeeChargesCharged(currency));
382+
lastRegularInstallment.setFeeChargesCharged(mergedFees.getAmount());
383+
384+
final Money mergedPenalties = lastRegularInstallment.getPenaltyChargesCharged(currency)
385+
.plus(additionalInstallment.getPenaltyChargesCharged(currency));
386+
lastRegularInstallment.setPenaltyCharges(mergedPenalties.getAmount());
387+
388+
additionalInstallment.getInstallmentCharges().forEach(charge -> {
389+
lastRegularInstallment.getInstallmentCharges().add(charge);
390+
charge.setInstallment(lastRegularInstallment);
391+
});
392+
});
393+
394+
installments.removeIf(i -> i.isAdditional() && i.getDueDate() != null && i.getDueDate().isBefore(newMaturityDate));
395+
});
396+
}
397+
316398
private void handleExtraRepaymentPeriod(final List<LoanRepaymentScheduleInstallment> installments,
317399
final LoanTermVariationsData termVariationsData, final ProgressiveLoanInterestScheduleModel scheduleModel) {
318400
final LocalDate interestRateChangeSubmittedOnDate = termVariationsData.getTermVariationApplicableFrom();
@@ -1266,7 +1348,7 @@ private List<ChangeOperation> createSortedChangeList(final List<LoanTermVariatio
12661348
.collect(Collectors.groupingBy(ltvd -> LoanTermVariationType.fromInt(ltvd.getTermType().getId().intValue())));
12671349

12681350
Stream.of(LoanTermVariationType.INTEREST_RATE_FROM_INSTALLMENT, LoanTermVariationType.INTEREST_PAUSE,
1269-
LoanTermVariationType.EXTEND_REPAYMENT_PERIOD).forEach(key -> {
1351+
LoanTermVariationType.EXTEND_REPAYMENT_PERIOD, LoanTermVariationType.DUE_DATE).forEach(key -> {
12701352
if (loanTermVariationsMap.get(key) != null) {
12711353
changeOperations.addAll(loanTermVariationsMap.get(key).stream().map(ChangeOperation::new).toList());
12721354
}

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.apache.fineract.organisation.monetary.domain.Money;
3838
import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
3939
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
40-
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
4140
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
4241
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
4342
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
@@ -106,11 +105,8 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer
106105
// generate list of proposed schedule due dates
107106
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
108107
periodStartDate, loanApplicationTerms, holidayDetailDTO);
109-
List<LoanTermVariationsData> loanTermVariations = loanApplicationTerms.getLoanTermVariations() != null
110-
? loanApplicationTerms.getLoanTermVariations().getExceptionData()
111-
: null;
112108
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel(
113-
expectedRepaymentPeriods, loanApplicationTerms.toLoanConfigurationDetails(), loanTermVariations,
109+
expectedRepaymentPeriods, loanApplicationTerms.toLoanConfigurationDetails(),
114110
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
115111
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
116112

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelServiceGsonContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public InterestPeriod createInterestPeriodInstance(Type type) {
5858
public ProgressiveLoanInterestScheduleModel createProgressiveLoanInterestScheduleModelInstance(Type type) {
5959
if (type == ProgressiveLoanInterestScheduleModel.class) {
6060
setPrev(null);
61-
return new ProgressiveLoanInterestScheduleModel(new ArrayList<>(), getLoanProductRelatedDetail(), new ArrayList<>(),
61+
return new ProgressiveLoanInterestScheduleModel(new ArrayList<>(), getLoanProductRelatedDetail(),
6262
installmentAmountInMultipliesOf, getMc());
6363
}
6464
throw new IllegalArgumentException("Unsupported ProgressiveLoanInterestScheduleModel type: " + type);

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.util.Optional;
2727
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
2828
import org.apache.fineract.organisation.monetary.domain.Money;
29-
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
3029
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
3130
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
3231
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
@@ -46,8 +45,7 @@ public interface EMICalculator {
4645
*/
4746
@NotNull
4847
ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
49-
@NotNull ILoanConfigurationDetails loanProductRelatedDetail, List<LoanTermVariationsData> loanTermVariations,
50-
Integer installmentAmountInMultiplesOf, MathContext mc);
48+
@NotNull ILoanConfigurationDetails loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
5149

5250
/**
5351
* This method creates an Interest model with repayment periods from the installments which retrieved from the
@@ -56,7 +54,7 @@ ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNul
5654
@NotNull
5755
ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
5856
@NotNull List<LoanRepaymentScheduleInstallment> installments, @NotNull ILoanConfigurationDetails loanProductRelatedDetail,
59-
List<LoanTermVariationsData> loanTermVariations, Integer installmentAmountInMultiplesOf, MathContext mc);
57+
Integer installmentAmountInMultiplesOf, MathContext mc);
6058

6159
/**
6260
* Find repayment period based on Due Date.
@@ -169,4 +167,7 @@ EqualAmortizationValues calculateEqualAmortizationValues(Money totalOutstanding,
169167
EqualAmortizationValues calculateAdjustedEqualAmortizationValues(Money outstanding, Money total,
170168
Money sumOfOtherEqualAmortizationValues, Integer numberOfInstallments, Integer installmentAmountInMultiplesOf,
171169
MonetaryCurrency currency);
170+
171+
void changeDueDate(ProgressiveLoanInterestScheduleModel scheduleModel, LoanApplicationTerms loanApplicationTerms,
172+
LocalDate targetRepaymentPeriodDueDate, LocalDate newDueDate);
172173
}

0 commit comments

Comments
 (0)