FINERACT-2322: Extend loan collection data with next due payment amount field
This commit is contained in:
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.delinquency.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class ProgressivePossibleNextRepaymentCalculationServiceImpl extends AbstractPossibleNextRepaymentCalculationService {
|
||||
|
||||
private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepository;
|
||||
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
|
||||
|
||||
@Override
|
||||
public BigDecimal calculateInterestRecalculationFutureOutstandingValue(Loan loan, LocalDate nextPaymentDueDate,
|
||||
LoanRepaymentScheduleInstallment nextInstallment) {
|
||||
MonetaryCurrency currency = loan.getCurrency();
|
||||
Optional<ProgressiveLoanModel> progressiveLoanModel = interestScheduleModelRepository.findOneByLoan(loan);
|
||||
Optional<ProgressiveLoanInterestScheduleModel> optionalScheduleModel = interestScheduleModelRepository
|
||||
.extractModel(progressiveLoanModel);
|
||||
ProgressiveLoanInterestScheduleModel scheduleModel = optionalScheduleModel.orElseGet(
|
||||
() -> advancedPaymentScheduleTransactionProcessor.calculateInterestScheduleModel(loan.getId(), nextPaymentDueDate));
|
||||
if (scheduleModel == null) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = loan.getRepaymentScheduleInstallments();
|
||||
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), repaymentScheduleInstallments, Set.of(),
|
||||
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), scheduleModel);
|
||||
ctx.setChargedOff(loan.isChargedOff());
|
||||
ctx.setContractTerminated(loan.isContractTermination());
|
||||
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(nextPaymentDueDate, ctx, false);
|
||||
RepaymentPeriod repaymentPeriod = scheduleModel.findRepaymentPeriodByDueDate(nextPaymentDueDate)
|
||||
.orElseGet(scheduleModel::getLastRepaymentPeriod);
|
||||
|
||||
return repaymentPeriod.getOutstandingPrincipal().add(repaymentPeriod.getOutstandingInterest())
|
||||
.add(nextInstallment.getFeeChargesOutstanding(currency)).add(nextInstallment.getPenaltyChargesOutstanding(currency))
|
||||
.getAmount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canAccept(Loan loan) {
|
||||
return loan.isProgressiveSchedule();
|
||||
}
|
||||
}
|
||||
+64
-71
@@ -1498,104 +1498,97 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
}
|
||||
|
||||
private List<LoanRepaymentScheduleInstallment> findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate targetDate,
|
||||
ProgressiveTransactionCtx transactionCtx) {
|
||||
return transactionCtx.getInstallments().stream() //
|
||||
.filter(installment -> !installment.isDownPayment() && !installment.isAdditional())
|
||||
.filter(installment -> installment.isOverdueOn(targetDate))
|
||||
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList();
|
||||
private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(LocalDate targetDate, ProgressiveLoanInterestScheduleModel model) {
|
||||
return model.repaymentPeriods().stream() //
|
||||
.filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).toList();
|
||||
}
|
||||
|
||||
public boolean rework(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
|
||||
boolean hasChange = false;
|
||||
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
|
||||
List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model);
|
||||
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
|
||||
RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod();
|
||||
RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod);
|
||||
MonetaryCurrency currency = model.zero().getCurrency();
|
||||
Money overDuePrincipal = Money.zero(currency);
|
||||
Money aggregatedOverDuePrincipal = Money.zero(currency);
|
||||
for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) {
|
||||
// add and subtract outstanding principal
|
||||
if (!overDuePrincipal.isZero()) {
|
||||
boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal,
|
||||
aggregatedOverDuePrincipal, ctx);
|
||||
|
||||
hasChange = hasChange || currentChanges;
|
||||
}
|
||||
|
||||
overDuePrincipal = processingPeriod.getOutstandingPrincipal();
|
||||
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
|
||||
}
|
||||
|
||||
if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) {
|
||||
boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal, aggregatedOverDuePrincipal,
|
||||
ctx);
|
||||
hasChange = hasChange || currentChanges;
|
||||
|
||||
}
|
||||
if (aggregatedOverDuePrincipal.isGreaterThanZero()
|
||||
&& (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) {
|
||||
model.lastOverdueBalanceChange(targetDate);
|
||||
}
|
||||
}
|
||||
|
||||
return hasChange;
|
||||
}
|
||||
|
||||
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
|
||||
recalculateInterestForDate(targetDate, ctx, true);
|
||||
}
|
||||
|
||||
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx, boolean updateInstallments) {
|
||||
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) {
|
||||
Loan loan = ctx.getInstallments().getFirst().getLoan();
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isNpa() && !ctx.isChargedOff()
|
||||
&& !ctx.isContractTerminated()) {
|
||||
&& !ctx.isContractTerminated() && !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(
|
||||
targetDate, ctx);
|
||||
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
|
||||
List<LoanRepaymentScheduleInstallment> normalInstallments = ctx.getInstallments().stream() //
|
||||
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment()).toList();
|
||||
|
||||
Optional<LoanRepaymentScheduleInstallment> currentInstallmentOptional = normalInstallments.stream().filter(
|
||||
installment -> installment.getFromDate().isBefore(targetDate) && !installment.getDueDate().isBefore(targetDate))
|
||||
.findAny();
|
||||
|
||||
// get DUE installment or last installment
|
||||
LoanRepaymentScheduleInstallment lastInstallment = normalInstallments.stream()
|
||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).get();
|
||||
LoanRepaymentScheduleInstallment currentInstallment = currentInstallmentOptional.orElse(lastInstallment);
|
||||
|
||||
Money overDuePrincipal = Money.zero(ctx.getCurrency());
|
||||
Money aggregatedOverDuePrincipal = Money.zero(ctx.getCurrency());
|
||||
for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) {
|
||||
// add and subtract outstanding principal
|
||||
if (!overDuePrincipal.isZero()) {
|
||||
adjustOverduePrincipalForInstallment(targetDate, processingInstallment, overDuePrincipal,
|
||||
aggregatedOverDuePrincipal, ctx);
|
||||
}
|
||||
|
||||
overDuePrincipal = processingInstallment.getPrincipalOutstanding(ctx.getCurrency());
|
||||
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
|
||||
}
|
||||
|
||||
boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(targetDate);
|
||||
if (adjustNeeded) {
|
||||
adjustOverduePrincipalForInstallment(targetDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal,
|
||||
ctx);
|
||||
}
|
||||
if (aggregatedOverDuePrincipal.isGreaterThanZero() && (ctx.getModel().lastOverdueBalanceChange() == null
|
||||
|| ctx.getModel().lastOverdueBalanceChange().isBefore(targetDate))) {
|
||||
ctx.getModel().lastOverdueBalanceChange(targetDate);
|
||||
}
|
||||
boolean modelHasUpdates = rework(targetDate, ctx);
|
||||
if (modelHasUpdates && updateInstallments) {
|
||||
updateInstallmentsPrincipalAndInterestByModel(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, LoanRepaymentScheduleInstallment currentInstallment,
|
||||
Money overduePrincipal, Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
|
||||
if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
|
||||
return;
|
||||
}
|
||||
private boolean adjustOverduePrincipal(LocalDate currentDate, RepaymentPeriod currentInstallment, Money overduePrincipal,
|
||||
Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
|
||||
|
||||
LocalDate fromDate = currentInstallment.getFromDate();
|
||||
LocalDate toDate = currentInstallment.getDueDate();
|
||||
boolean hasUpdate = false;
|
||||
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
|
||||
boolean prepayAttempt = ctx.isPrepayAttempt();
|
||||
LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan()
|
||||
.getLoanInterestRecalculationDetails();
|
||||
|
||||
if (!currentDate.equals(ctx.getModel().lastOverdueBalanceChange())) {
|
||||
// if we have same date for fromDate & last overdue balance change then it means we have the up-to-date
|
||||
// model.
|
||||
if (ctx.getModel().lastOverdueBalanceChange() == null
|
||||
|| currentInstallment.getFromDate().isAfter(ctx.getModel().lastOverdueBalanceChange())) {
|
||||
// first overdue hit for installment. setting overdue balance correction from instalment from date.
|
||||
emiCalculator.addBalanceCorrection(ctx.getModel(), fromDate, overduePrincipal);
|
||||
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
|
||||
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
|
||||
emiCalculator.addBalanceCorrection(model, fromDate, overduePrincipal);
|
||||
} else {
|
||||
// not the first balance correction on installment period, then setting overdue balance correction from
|
||||
// last balance change's current date. previous interest period already has the correct balance
|
||||
// correction.
|
||||
emiCalculator.addBalanceCorrection(ctx.getModel(), ctx.getModel().lastOverdueBalanceChange(), overduePrincipal);
|
||||
emiCalculator.addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
|
||||
}
|
||||
|
||||
hasUpdate = true;
|
||||
|
||||
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
|
||||
LocalDate lastOverdueBalanceChange;
|
||||
if (shouldRecalculateTillInstallmentDueDate(currentInstallment.getLoan().getLoanInterestRecalculationDetails(),
|
||||
ctx.isPrepayAttempt())) {
|
||||
if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) {
|
||||
lastOverdueBalanceChange = toDate;
|
||||
} else {
|
||||
lastOverdueBalanceChange = currentDate;
|
||||
}
|
||||
emiCalculator.addBalanceCorrection(ctx.getModel(), lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||
ctx.getModel().lastOverdueBalanceChange(lastOverdueBalanceChange);
|
||||
emiCalculator.addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasUpdate) {
|
||||
updateInstallmentsPrincipalAndInterestByModel(ctx);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) {
|
||||
|
||||
+3
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.portfolio.loanaccount.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -27,4 +28,6 @@ public interface ProgressiveLoanModelRepository
|
||||
extends JpaSpecificationExecutor<ProgressiveLoanModel>, JpaRepository<ProgressiveLoanModel, Long> {
|
||||
|
||||
Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
|
||||
|
||||
Optional<ProgressiveLoanModel> findOneByLoan(Loan loan);
|
||||
}
|
||||
|
||||
+2
@@ -29,6 +29,8 @@ public interface InterestScheduleModelRepositoryWrapper {
|
||||
|
||||
Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
|
||||
|
||||
Optional<ProgressiveLoanModel> findOneByLoan(Loan loan);
|
||||
|
||||
Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel);
|
||||
|
||||
ProgressiveLoanInterestScheduleModel writeInterestScheduleModel(Loan loan, ProgressiveLoanInterestScheduleModel model);
|
||||
|
||||
+10
@@ -23,6 +23,7 @@ import jakarta.transaction.Transactional;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
@@ -78,6 +79,15 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
|
||||
return progressiveLoanModel[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProgressiveLoanModel> findOneByLoan(Loan loan) {
|
||||
AtomicReference<Optional<ProgressiveLoanModel>> progressiveLoanModelRef = new AtomicReference<>();
|
||||
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
|
||||
progressiveLoanModelRef.set(loanModelRepository.findOneByLoan(loan));
|
||||
});
|
||||
return progressiveLoanModelRef.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel) {
|
||||
return progressiveLoanModel.map(ProgressiveLoanModel::getJsonModel) //
|
||||
|
||||
+9
@@ -381,6 +381,15 @@ public final class RepaymentPeriod {
|
||||
return previous == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back getDueInterest minus paid interest
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getOutstandingInterest() {
|
||||
return MathUtil.negativeToZero(getDueInterest().minus(getPaidInterest()), mc);
|
||||
}
|
||||
|
||||
public Money getOutstandingPrincipal() {
|
||||
return MathUtil.negativeToZero(getDuePrincipal().minus(getPaidPrincipal()), mc);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user