FINERACT-2354: Re-aging: -Interest Handling Option: Equal amortization

This commit is contained in:
Soma Sörös
2025-11-21 15:02:34 +01:00
committed by Adam Saghy
parent 47e8674fa0
commit 747121bbe2
17 changed files with 3125 additions and 120 deletions
@@ -461,4 +461,17 @@ public final class DateUtils {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors, e); throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", errors, e);
} }
} }
/**
* Returns the earlier date. If date1 is before date2 it return date1 otherwise date2.
*
* @param date1
* non null date1
* @param date2
* non null date2
* @return earlier date
*/
public static LocalDate min(@NonNull LocalDate date1, @NonNull LocalDate date2) {
return date1.isBefore(date2) ? date1 : date2;
}
} }
File diff suppressed because it is too large Load Diff
@@ -261,9 +261,21 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
} }
public static LoanRepaymentScheduleInstallment newReAgedInstallment(final Loan loan, final Integer installmentNumber, public static LoanRepaymentScheduleInstallment newReAgedInstallment(final Loan loan, final Integer installmentNumber,
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal) { final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal fees,
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal, null, null, null, null, null, final BigDecimal penalties, final BigDecimal interestAccrued, final BigDecimal feeAccrued, final BigDecimal penaltyAccrued) {
null, null, false, false, true); LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate,
principal, interest, fees, penalties, null, null, null, null, false, false, true);
installment.setInterestAccrued(interestAccrued);
installment.setFeeAccrued(feeAccrued);
installment.setPenaltyAccrued(penaltyAccrued);
return installment;
}
public static LoanRepaymentScheduleInstallment newReAgedInstallment(final Loan loan, final Integer installmentNumber,
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal fees,
final BigDecimal penalties) {
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal, interest, fees, penalties, null,
null, null, null, false, false, true);
} }
public static LoanRepaymentScheduleInstallment getLastNonDownPaymentInstallment(List<LoanRepaymentScheduleInstallment> installments) { public static LoanRepaymentScheduleInstallment getLastNonDownPaymentInstallment(List<LoanRepaymentScheduleInstallment> installments) {
@@ -43,6 +43,7 @@ import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -57,6 +58,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.NotImplementedException;
@@ -92,12 +94,14 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter; import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; 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.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanConfigurationDetailsMapper; import org.apache.fineract.portfolio.loanaccount.mapper.LoanConfigurationDetailsMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
@@ -105,6 +109,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -115,6 +120,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.DueType;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.apache.fineract.portfolio.util.InstallmentProcessingHelper;
import org.apache.fineract.util.LoopContext; import org.apache.fineract.util.LoopContext;
import org.apache.fineract.util.LoopGuard; import org.apache.fineract.util.LoopGuard;
@@ -129,17 +135,19 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private final LoanScheduleComponent loanSchedule; private final LoanScheduleComponent loanSchedule;
private final LoanChargeService loanChargeService; private final LoanChargeService loanChargeService;
private final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeRepaymentScheduleProcessing; private final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeRepaymentScheduleProcessing;
private final ScheduledDateGenerator scheduledDateGenerator;
public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final InterestRefundService interestRefundService, public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final InterestRefundService interestRefundService,
final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule, final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService, final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService,
final LoanChargeService loanChargeService) { final LoanChargeService loanChargeService, ScheduledDateGenerator scheduledDateGenerator) {
super(externalIdFactory, loanChargeValidator, loanBalanceService); super(externalIdFactory, loanChargeValidator, loanBalanceService);
this.emiCalculator = emiCalculator; this.emiCalculator = emiCalculator;
this.interestRefundService = interestRefundService; this.interestRefundService = interestRefundService;
this.loanSchedule = loanSchedule; this.loanSchedule = loanSchedule;
this.loanChargeService = loanChargeService; this.loanChargeService = loanChargeService;
this.loanChargeRepaymentScheduleProcessing = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); this.loanChargeRepaymentScheduleProcessing = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
this.scheduledDateGenerator = scheduledDateGenerator;
} }
@Override @Override
@@ -466,15 +474,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
} else { } else {
if (ctx instanceof ProgressiveTransactionCtx progCtx) { if (ctx instanceof ProgressiveTransactionCtx progCtx) {
final Money interestBeforeRefund = emiCalculator.getSumOfDueInterestsOnDate(progCtx.getModel(), LocalDate targetDate = loanTransaction.getDateOf();
loanTransaction.getDateOf()); final Money interestBeforeRefund = emiCalculator.getSumOfDueInterestsOnDate(progCtx.getModel(), targetDate);
final List<Long> unmodifiedTransactionIds = progCtx.getAlreadyProcessedTransactions().stream() final List<Long> unmodifiedTransactionIds = progCtx.getAlreadyProcessedTransactions().stream()
.filter(LoanTransaction::isNotReversed).map(AbstractPersistableCustom::getId).toList(); .filter(LoanTransaction::isNotReversed).map(AbstractPersistableCustom::getId).toList();
final List<LoanTransaction> modifiedTransactions = new ArrayList<>(progCtx.getAlreadyProcessedTransactions().stream() final List<LoanTransaction> modifiedTransactions = new ArrayList<>(progCtx.getAlreadyProcessedTransactions().stream()
.filter(LoanTransaction::isNotReversed).filter(tr -> tr.getId() == null).toList()); .filter(LoanTransaction::isNotReversed).filter(tr -> tr.getId() == null).toList());
if (!modifiedTransactions.isEmpty()) { if (!modifiedTransactions.isEmpty()) {
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate,
loanTransaction.getDateOf(), modifiedTransactions, unmodifiedTransactionIds); modifiedTransactions, unmodifiedTransactionIds);
final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund); final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
loanTransaction.updateAmount(newAmount.getAmount()); loanTransaction.updateAmount(newAmount.getAmount());
} }
@@ -2291,8 +2299,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
HorizontalPaymentAllocationContext paymentAllocationContext = new HorizontalPaymentAllocationContext(ctx, loanTransaction, HorizontalPaymentAllocationContext paymentAllocationContext = new HorizontalPaymentAllocationContext(ctx, loanTransaction,
paymentAllocationTypes, futureInstallmentAllocationRule, transactionMappings, balances); paymentAllocationTypes, futureInstallmentAllocationRule, transactionMappings, balances);
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed); paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
boolean interestBearingAndInterestRecalculationEnabled = loanTransaction.getLoan()
.isInterestBearingAndInterestRecalculationEnabled();
if (isInterestRecalculationSupported(ctx, loanTransaction.getLoan())) { if (isInterestRecalculationSupported(ctx, loanTransaction.getLoan())) {
// Clear any previously skipped installments before re-evaluating // Clear any previously skipped installments before re-evaluating
@@ -2592,6 +2598,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
final boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber); final boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber);
return charges.stream() return charges.stream()
.filter(loanCharge -> (loanCharge.isInstalmentFee() && loanCharge.hasInstallmentFor(installment)) .filter(loanCharge -> (loanCharge.isInstalmentFee() && loanCharge.hasInstallmentFor(installment))
|| (installment.isReAged() && loanCharge.hasInstallmentFor(installment))
|| loanCharge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment)) || loanCharge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@@ -2760,6 +2767,39 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private Money aggregatedPenaltyChargesPortion; private Money aggregatedPenaltyChargesPortion;
} }
@AllArgsConstructor
@Getter
@Setter
private static final class BalancesWithPaidInAdvance {
private Money principal;
private Money interest;
private Money fee;
private Money penalty;
private Money paidInAdvance;
private Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings;
private BalancesWithPaidInAdvance(MonetaryCurrency currency) {
this(Money.zero(currency), Money.zero(currency), Money.zero(currency), Money.zero(currency), Money.zero(currency),
new LinkedHashSet<>());
}
private BalancesWithPaidInAdvance(LoanRepaymentScheduleInstallment i, MonetaryCurrency currency) {
this(i.getPrincipalCompleted(currency), i.getInterestPaid(currency), i.getFeeChargesPaid(currency),
i.getPenaltyChargesPaid(currency), i.getTotalPaidInAdvance(currency),
new LinkedHashSet<>(i.getLoanTransactionToRepaymentScheduleMappings()));
}
private static BalancesWithPaidInAdvance summarizerAccumulator(BalancesWithPaidInAdvance a, BalancesWithPaidInAdvance b) {
Set<LoanTransactionToRepaymentScheduleMapping> set = new LinkedHashSet<>(
a.getLoanTransactionToRepaymentScheduleMappings().size() + b.getLoanTransactionToRepaymentScheduleMappings().size());
set.addAll(a.getLoanTransactionToRepaymentScheduleMappings());
set.addAll(b.getLoanTransactionToRepaymentScheduleMappings());
return new BalancesWithPaidInAdvance(a.getPrincipal().add(b.getPrincipal()), a.getInterest().add(b.getInterest()),
a.getFee().add(b.getFee()), a.getPenalty().add(b.getPenalty()), a.getPaidInAdvance().add(b.getPaidInAdvance()), set);
}
}
private void mergeReAgedInstallment(final LoanRepaymentScheduleInstallment target, private void mergeReAgedInstallment(final LoanRepaymentScheduleInstallment target,
final LoanRepaymentScheduleInstallment reAgedInstallment, MonetaryCurrency currency, LocalDate transactionDate) { final LoanRepaymentScheduleInstallment reAgedInstallment, MonetaryCurrency currency, LocalDate transactionDate) {
target.setAdditional(false); target.setAdditional(false);
@@ -2767,11 +2807,19 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
target.setFromDate(reAgedInstallment.getFromDate()); target.setFromDate(reAgedInstallment.getFromDate());
target.setDueDate(reAgedInstallment.getDueDate()); target.setDueDate(reAgedInstallment.getDueDate());
target.setPrincipal(MathUtil.add(reAgedInstallment.getPrincipal(), target.getPrincipalCompleted())); target.setPrincipal(MathUtil.add(reAgedInstallment.getPrincipal(), target.getPrincipalCompleted()));
target.setInterestCharged(MathUtil.add(reAgedInstallment.getInterestCharged(), target.getInterestPaid())); target.setInterestCharged(MathUtil.add(reAgedInstallment.getInterestCharged(), target.getInterestPaid()));
target.setInterestAccrued(MathUtil.add(target.getInterestAccrued(), reAgedInstallment.getInterestAccrued()));
target.setFeeChargesCharged(MathUtil.add(reAgedInstallment.getFeeChargesCharged(), target.getFeeChargesCharged()));
target.setFeeAccrued(MathUtil.add(target.getFeeAccrued(), reAgedInstallment.getFeeAccrued()));
target.setPenaltyCharges(MathUtil.add(reAgedInstallment.getPenaltyCharges(), target.getPenaltyCharges()));
target.setPenaltyAccrued(MathUtil.add(target.getPenaltyAccrued(), reAgedInstallment.getPenaltyAccrued()));
target.updateObligationsMet(currency, transactionDate); target.updateObligationsMet(currency, transactionDate);
} }
private void insertOrReplaceRelatedInstallment(List<LoanRepaymentScheduleInstallment> installments, private LoanRepaymentScheduleInstallment insertOrReplaceRelatedInstallment(List<LoanRepaymentScheduleInstallment> installments,
final LoanRepaymentScheduleInstallment reAgedInstallment, final MonetaryCurrency currency, final LocalDate transactionDate) { final LoanRepaymentScheduleInstallment reAgedInstallment, final MonetaryCurrency currency, final LocalDate transactionDate) {
Optional<LoanRepaymentScheduleInstallment> first = installments.stream() Optional<LoanRepaymentScheduleInstallment> first = installments.stream()
.filter(installment -> Objects.equals(installment.getInstallmentNumber(), reAgedInstallment.getInstallmentNumber())) .filter(installment -> Objects.equals(installment.getInstallmentNumber(), reAgedInstallment.getInstallmentNumber()))
@@ -2785,107 +2833,71 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// additional ( N+1 ) installment due date cannot be earlier than its original due date // additional ( N+1 ) installment due date cannot be earlier than its original due date
if (!target.getDueDate().isAfter(reAgedInstallment.getDueDate())) { if (!target.getDueDate().isAfter(reAgedInstallment.getDueDate())) {
mergeReAgedInstallment(target, reAgedInstallment, currency, transactionDate); mergeReAgedInstallment(target, reAgedInstallment, currency, transactionDate);
return target;
} else { } else {
InstallmentProcessingHelper.addOneToInstallmentNumberFromInstallment(installments,
reAgedInstallment.getInstallmentNumber());
installments.add(reAgedInstallment); installments.add(reAgedInstallment);
reAgedInstallment.updateObligationsMet(currency, transactionDate); reAgedInstallment.updateObligationsMet(currency, transactionDate);
installments.stream() return reAgedInstallment;
.filter(i -> i.getInstallmentNumber() != null && i.getInstallmentNumber() >= target.getInstallmentNumber())
.forEach(i -> i.setInstallmentNumber(i.getInstallmentNumber() + 1));
} }
} else { } else {
mergeReAgedInstallment(target, reAgedInstallment, currency, transactionDate); mergeReAgedInstallment(target, reAgedInstallment, currency, transactionDate);
return target;
} }
} else { } else {
installments.add(reAgedInstallment); installments.add(reAgedInstallment);
reAgedInstallment.updateObligationsMet(currency, transactionDate); reAgedInstallment.updateObligationsMet(currency, transactionDate);
return reAgedInstallment;
} }
} }
private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) { private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) {
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
MonetaryCurrency currency = ctx.getCurrency(); Loan loan = loanTransaction.getLoan();
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments(); LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
// re-aging logic for interest-bearing loans if (loan.isInterestBearing()) {
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx if (((loanReAgeParameter.getInterestHandlingType() == null)
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) { || loanReAgeParameter.getInterestHandlingType().equals(LoanReAgeInterestHandlingType.DEFAULT))) {
handleReAgeWithInterestRecalculationEnabled(loanTransaction, progressiveTransactionCtx);
} else if (loanTransaction.getLoan().isInterestBearing() && !loanTransaction.getLoan().isInterestRecalculationEnabled()) { // re-aging logic for interest-bearing loans
// TODO: implement interestRecalculation = false logic if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
throw new NotImplementedException( && loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
"Logic for re-aging when interest bearing loan has interestRecalculation disabled is not implemented"); handleReAgeWithInterestRecalculationEnabled(loanTransaction, progressiveTransactionCtx);
} else { } else if (loanTransaction.getLoan().isInterestBearing() && !loanTransaction.getLoan().isInterestRecalculationEnabled()) {
AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency)); // TODO: implement interestRecalculation = false logic
installments.forEach(i -> { throw new NotImplementedException(
Money principalOutstanding = i.getPrincipalOutstanding(currency); "Logic for re-aging when interest bearing loan has interestRecalculation disabled is not implemented");
if (principalOutstanding.isGreaterThanZero()) {
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated());
} }
});
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency), } else if (LoanReAgeInterestHandlingType.WAIVE_INTEREST.equals(loanReAgeParameter.getInterestHandlingType())) {
Money.zero(currency)); throw new NotImplementedException("WAIVE_INTEREST interest handling strategy for re-aging is not implemented");
if (outstandingPrincipalBalance.get().isZero()) {
loanTransaction.reverse();
return;
}
Money calculatedPrincipal = Money.zero(currency);
Money adjustCalculatedPrincipal = Money.zero(currency);
if (outstandingPrincipalBalance.get().isGreaterThanZero()) {
calculatedPrincipal = outstandingPrincipalBalance.get()
.dividedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(), MoneyHelper.getMathContext());
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail()
.getInstallmentAmountInMultiplesOf();
if (installmentAmountInMultiplesOf != null) {
calculatedPrincipal = Money.roundToMultiplesOf(calculatedPrincipal, installmentAmountInMultiplesOf);
}
adjustCalculatedPrincipal = outstandingPrincipalBalance.get()
.minus(calculatedPrincipal.multipliedBy(loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()));
}
Optional<LoanRepaymentScheduleInstallment> lastNormalInstallmentOptional = installments.stream().filter(i -> !i.isDownPayment())
.filter(i -> i.getDueDate().isBefore(loanTransaction.getTransactionDate())).reduce((first, second) -> second);
int reAgedInstallmentNumber;
LocalDate fromDate;
Loan loan;
if (lastNormalInstallmentOptional.isEmpty()) {
LoanRepaymentScheduleInstallment firstNormalInstallment = installments.stream().filter(i -> !i.isDownPayment())
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).orElseThrow();
reAgedInstallmentNumber = firstNormalInstallment.getInstallmentNumber();
fromDate = firstNormalInstallment.getFromDate();
loan = firstNormalInstallment.getLoan();
} else { } else {
LoanRepaymentScheduleInstallment lastNormalInstallment = lastNormalInstallmentOptional.get(); if (LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.equals(loanReAgeParameter.getInterestHandlingType())) {
reAgedInstallmentNumber = lastNormalInstallment.getInstallmentNumber() + 1; CommonReAgeSettings settings = new CommonReAgeSettings(false, true, true, true);
fromDate = lastNormalInstallment.getDueDate(); if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
loan = lastNormalInstallment.getLoan(); && loanTransaction.getLoan().isInterestRecalculationEnabled()) {
} handleReAgeEqualAmortizationEMICalculator(loanTransaction, settings, progressiveTransactionCtx);
} else {
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan, handleReAgeWithCommonStrategy(loanTransaction, settings, ctx);
reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(), }
calculatedPrincipal.getAmount()); } else if (LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate()); .equals(loanReAgeParameter.getInterestHandlingType())) {
CommonReAgeSettings settings = new CommonReAgeSettings(true, true, true, true);
for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) { if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(), && loanTransaction.getLoan().isInterestRecalculationEnabled()) {
reAgedInstallment.getDueDate()); handleReAgeEqualAmortizationEMICalculator(loanTransaction, settings, progressiveTransactionCtx);
int nextReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber() + 1; } else {
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(), handleReAgeWithCommonStrategy(loanTransaction, settings, ctx);
nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate, calculatedPrincipal.getAmount()); }
if (i + 1 == loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) {
reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal);
} }
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate());
} }
int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber(); } else {
List<LoanRepaymentScheduleInstallment> toRemove = installments.stream().filter(i -> i != null && !i.isAdditional() handleReAgeWithCommonStrategy(loanTransaction, new CommonReAgeSettings(), ctx);
&& i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber).toList(); }
toRemove.forEach(installments::remove); if (loanTransaction.getAmount().compareTo(ZERO) == 0) {
reprocessInstallments(installments); loanTransaction.reverse();
} }
} }
@@ -2929,7 +2941,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
final LoanRepaymentScheduleInstallment lastInstallment = ctx.getInstallments().getLast(); final LoanRepaymentScheduleInstallment lastInstallment = ctx.getInstallments().getLast();
final LoanRepaymentScheduleInstallment newInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment( final LoanRepaymentScheduleInstallment newInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(
loanTransaction.getLoan(), lastInstallment.getInstallmentNumber() + 1, rp.getFromDate(), rp.getDueDate(), loanTransaction.getLoan(), lastInstallment.getInstallmentNumber() + 1, rp.getFromDate(), rp.getDueDate(),
rp.getDuePrincipal().getAmount()); rp.getDuePrincipal().getAmount(), null, null, null);
if (rp.getDueInterest().isGreaterThanZero()) { if (rp.getDueInterest().isGreaterThanZero()) {
newInstallment.addToInterest(transactionDate, rp.getDueInterest()); newInstallment.addToInterest(transactionDate, rp.getDueInterest());
@@ -3193,6 +3205,294 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
} }
private BalancesWithPaidInAdvance liftEarlyRepaidBalances(List<LoanRepaymentScheduleInstallment> installments,
LocalDate transactionDate, MonetaryCurrency currency, List<LoanTransaction> alreadyProcessedTransactions) {
return installments.stream().filter(i -> !i.isDownPayment() && !i.isAdditional() && !i.getDueDate().isBefore(transactionDate))
.map(installment -> {
alreadyProcessedTransactions.forEach(tr -> {
Set<LoanTransactionToRepaymentScheduleMapping> relatedMapping = tr.getLoanTransactionToRepaymentScheduleMappings()
.stream().filter(m -> m.getInstallment().equals(installment)).collect(Collectors.toSet());
installment.getLoanTransactionToRepaymentScheduleMappings().addAll(relatedMapping);
});
BalancesWithPaidInAdvance res = new BalancesWithPaidInAdvance(installment, currency);
installment.resetDerivedComponents();
installment.getLoanTransactionToRepaymentScheduleMappings()
.forEach(m -> m.getLoanTransaction().getLoanTransactionToRepaymentScheduleMappings().remove(m));
installment.getLoanTransactionToRepaymentScheduleMappings().clear();
return res;
}).reduce(new BalancesWithPaidInAdvance(currency), BalancesWithPaidInAdvance::summarizerAccumulator);
}
private void handleReAgeEqualAmortizationEMICalculator(LoanTransaction loanTransaction, CommonReAgeSettings settings,
ProgressiveTransactionCtx ctx) {
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
MonetaryCurrency currency = ctx.getCurrency();
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
Integer numberOfReAgeInstallments = loanReAgeParameter.getNumberOfInstallments();
LocalDate transactionDate = loanTransaction.getTransactionDate();
OutstandingDetails outstandingDetails = emiCalculator.precalculateReAgeEqualAmortizationAmount(model, transactionDate,
loanReAgeParameter);
OutstandingBalances outstandingBalances = liftOutstandingBalances(installments, transactionDate, currency,
settings.isSkipDownPayments(), settings.isOnlyPayableInterest(), settings.isEqualInstallmentForInterest(),
settings.isEqualInstallmentForFeesAndPenalties(), ctx);
loanTransaction.updateComponentsAndTotal(outstandingDetails.getOutstandingPrincipal(), outstandingDetails.getOutstandingInterest(),
outstandingBalances.fees, outstandingBalances.penalties);
if (loanTransaction.getAmount().compareTo(ZERO) == 0) {
loanTransaction.reverse();
}
// handle non EMI calculator portions
EqualAmortizationValues calculatedFees = emiCalculator.calculateEqualAmortizationValues(outstandingBalances.fees,
numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedPenalties = emiCalculator.calculateEqualAmortizationValues(outstandingBalances.penalties,
numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedInterestAccrued = emiCalculator
.calculateEqualAmortizationValues(outstandingBalances.interestAccrued, numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedFeeAccrued = emiCalculator.calculateEqualAmortizationValues(outstandingBalances.feesAccrued,
numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedPenaltyAccrued = emiCalculator
.calculateEqualAmortizationValues(outstandingBalances.penaltiesAccrued, numberOfReAgeInstallments, null, currency);
List<ReAgedChargeEqualAmortizationValues> calculatedCharges = outstandingBalances.liftedLoanCharges().stream()
.map(loanCharge -> new ReAgedChargeEqualAmortizationValues(loanCharge, emiCalculator.calculateEqualAmortizationValues(
loanCharge.getAmountOutstanding(currency), numberOfReAgeInstallments, null, currency)))
.toList();
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()),
calculatedFees.adjustment().add(calculatedPenalties.adjustment())));
installments.removeIf(i -> (i.getInstallmentNumber() != null && !i.isDownPayment() && !i.getDueDate().isBefore(transactionDate)
&& !i.isAdditional()) || (!i.getDueDate().isAfter(model.getMaturityDate()) && i.isAdditional()));
installments.stream().filter(LoanRepaymentScheduleInstallment::isAdditional).forEach(i -> {
i.setFromDate(model.getMaturityDate());
i.setInstallmentNumber(model.repaymentPeriods().size());
});
for (int index = 0; index < model.repaymentPeriods().size(); index++) {
RepaymentPeriod rp = model.repaymentPeriods().get(index);
if (rp.getDueDate().isBefore(transactionDate)) {
// update existing
Optional<LoanRepaymentScheduleInstallment> notReagedInstallment = installments.stream()
.filter(i -> i.getDueDate().isEqual(rp.getDueDate()) && i.getFromDate().isEqual(rp.getFromDate())).findFirst();
LoanRepaymentScheduleInstallment installment = notReagedInstallment.orElseThrow();
installment.setInterestCharged(installment.getInterestPaid());
installment.setPrincipal(installment.getPrincipalCompleted(currency).getAmount());
installment.setInstallmentNumber(index + 1);
installment.updateObligationsMet(currency, transactionDate);
// TODO add remaining components
} else {
LoanRepaymentScheduleInstallment created = LoanRepaymentScheduleInstallment.newReAgedInstallment(loanTransaction.getLoan(),
index + 1, rp.getFromDate(), rp.getDueDate(), rp.getDuePrincipal().getAmount(), rp.getDueInterest().getAmount(),
ZERO, ZERO);
if (rp.isReAgedEarlyRepaymentHolder()) {
created.setPrincipalCompleted(rp.getPaidPrincipal().getAmount());
created.setInterestPaid(rp.getPaidInterest().getAmount());
created.setFeeChargesCharged(paidInAdvanceBalances.getFee().getAmount());
created.setFeeChargesPaid(paidInAdvanceBalances.getFee().getAmount());
created.setPenaltyCharges(paidInAdvanceBalances.getPenalty().getAmount());
created.setPenaltyChargesPaid(paidInAdvanceBalances.getPenalty().getAmount());
created.setTotalPaidInAdvance(paidInAdvanceBalances.getPaidInAdvance().getAmount());
paidInAdvanceBalances.loanTransactionToRepaymentScheduleMappings.forEach(m -> m.setInstallment(created));
} else {
boolean isLastRepaymentPeriod = model.isLastRepaymentPeriod(rp);
created.setFeeChargesCharged(calculatedFees.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setPenaltyCharges(calculatedPenalties.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setInterestAccrued(calculatedInterestAccrued.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setFeeAccrued(calculatedFeeAccrued.calculateValueBigDecimal(isLastRepaymentPeriod));
created.setPenaltyAccrued(calculatedPenaltyAccrued.calculateValueBigDecimal(isLastRepaymentPeriod));
createChargeMappingsForInstallment(created, calculatedCharges, isLastRepaymentPeriod);
}
created.updateObligationsMet(currency, transactionDate);
installments.add(created);
}
}
reprocessInstallments(installments);
}
private void handleReAgeWithCommonStrategy(LoanTransaction loanTransaction, CommonReAgeSettings settings, TransactionCtx ctx) {
MonetaryCurrency currency = ctx.getCurrency();
Loan loan = loanTransaction.getLoan();
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
LocalDate transactionDate = loanTransaction.getTransactionDate();
Integer numberOfReAgeInstallments = loanReAgeParameter.getNumberOfInstallments();
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProductRelatedDetail()
.getInstallmentAmountInMultiplesOf();
OutstandingBalances outstandingBalances = liftOutstandingBalances(installments, transactionDate, currency,
settings.isSkipDownPayments(), settings.isOnlyPayableInterest(), settings.isEqualInstallmentForInterest(),
settings.isEqualInstallmentForFeesAndPenalties(), ctx);
loanTransaction.updateComponentsAndTotal(outstandingBalances.principal, outstandingBalances.interest, outstandingBalances.fees,
outstandingBalances.penalties);
if (MathUtil.isZero(loanTransaction.getAmount())) {
loanTransaction.reverse();
return;
}
EqualAmortizationValues calculatedInterest = emiCalculator.calculateEqualAmortizationValues(outstandingBalances.interest,
numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedFees = emiCalculator.calculateEqualAmortizationValues(outstandingBalances.fees,
numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedPenalties = emiCalculator.calculateEqualAmortizationValues(outstandingBalances.penalties,
numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedInterestAccrued = emiCalculator
.calculateEqualAmortizationValues(outstandingBalances.interestAccrued, numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedFeeAccrued = emiCalculator.calculateEqualAmortizationValues(outstandingBalances.feesAccrued,
numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedPenaltyAccrued = emiCalculator
.calculateEqualAmortizationValues(outstandingBalances.penaltiesAccrued, numberOfReAgeInstallments, null, currency);
EqualAmortizationValues calculatedPrincipal = emiCalculator.calculateAdjustedEqualAmortizationValues(outstandingBalances.principal,
outstandingBalances.principal.add(outstandingBalances.interest).add(outstandingBalances.fees)
.add(outstandingBalances.penalties),
calculatedInterest.value().add(calculatedFees.value()).add(calculatedPenalties.value()), numberOfReAgeInstallments,
installmentAmountInMultiplesOf, currency);
List<ReAgedChargeEqualAmortizationValues> calculatedCharges = outstandingBalances.liftedLoanCharges().stream()
.map(loanCharge -> new ReAgedChargeEqualAmortizationValues(loanCharge, emiCalculator.calculateEqualAmortizationValues(
loanCharge.getAmountOutstanding(currency), numberOfReAgeInstallments, null, currency)))
.toList();
FirstReAgeInstallmentProps firstReAgeInstallmentProps = calculateFirstReAgeInstallmentProps(installments,
loanReAgeParameter.getStartDate());
BalancesWithPaidInAdvance balances = installments.stream()
.filter(i -> !i.isDownPayment() && !i.isAdditional() && !i.getDueDate().isBefore(transactionDate)).map(installment -> {
BalancesWithPaidInAdvance res = new BalancesWithPaidInAdvance(installment, currency);
installment.setPrincipal(
installment.getPrincipal(currency).minus(installment.getPrincipalCompleted(currency)).getAmount());
installment.setPrincipalCompleted(null);
installment.setInterestCharged(
installment.getInterestCharged(currency).minus(installment.getInterestPaid(currency)).getAmount());
installment.setInterestPaid(null);
installment.setFeeChargesCharged(
installment.getFeeChargesCharged(currency).minus(installment.getFeeChargesPaid(currency)).getAmount());
installment.setFeeChargesPaid(null);
installment.setPenaltyCharges(
installment.getPenaltyChargesCharged(currency).minus(installment.getPenaltyChargesPaid(currency)).getAmount());
installment.setPenaltyChargesPaid(null);
installment.setTotalPaidInAdvance(null);
return res;
}).reduce(new BalancesWithPaidInAdvance(currency), BalancesWithPaidInAdvance::summarizerAccumulator);
if (!balances.getPrincipal().isZero() || !balances.getInterest().isZero() || !balances.getFee().isZero()
|| !balances.getPenalty().isZero()) {
final LoanRepaymentScheduleInstallment earlyRepaidInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
firstReAgeInstallmentProps.reAgedInstallmentNumber(), firstReAgeInstallmentProps.fromDate(), transactionDate,
balances.getPrincipal().getAmount(), balances.getInterest().getAmount(), balances.getFee().getAmount(),
balances.getPenalty().getAmount(), null, null, null);
earlyRepaidInstallment.setPrincipalCompleted(balances.getPrincipal().getAmount());
earlyRepaidInstallment.setInterestPaid(balances.getInterest().getAmount());
earlyRepaidInstallment.setFeeChargesPaid(balances.getFee().getAmount());
earlyRepaidInstallment.setPenaltyChargesPaid(balances.getPenalty().getAmount());
earlyRepaidInstallment.setTotalPaidInAdvance(balances.getPaidInAdvance().getAmount());
earlyRepaidInstallment.updateObligationsMet(currency, transactionDate);
firstReAgeInstallmentProps = new FirstReAgeInstallmentProps(firstReAgeInstallmentProps.reAgedInstallmentNumber + 1,
transactionDate);
InstallmentProcessingHelper.addOneToInstallmentNumberFromInstallment(installments,
earlyRepaidInstallment.getInstallmentNumber());
loan.getRepaymentScheduleInstallments().add(earlyRepaidInstallment);
}
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
firstReAgeInstallmentProps.reAgedInstallmentNumber, firstReAgeInstallmentProps.fromDate, loanReAgeParameter.getStartDate(),
calculatedPrincipal.value().getAmount(), calculatedInterest.value().getAmount(), calculatedFees.value().getAmount(),
calculatedPenalties.value().getAmount(), calculatedInterestAccrued.value().getAmount(),
calculatedFeeAccrued.value().getAmount(), calculatedPenaltyAccrued.value().getAmount());
reAgedInstallment = insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, transactionDate);
createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, false);
for (int i = 1; i < numberOfReAgeInstallments; i++) {
LocalDate calculatedDueDate = scheduledDateGenerator.getRepaymentPeriodDate(loanReAgeParameter.getFrequencyType(),
loanReAgeParameter.getFrequencyNumber(), reAgedInstallment.getDueDate());
calculateReAgedInstallmentDueDate(loanReAgeParameter, reAgedInstallment.getDueDate());
int nextReAgedInstallmentNumber = firstReAgeInstallmentProps.reAgedInstallmentNumber + i;
boolean isLastInstallment = i + 1 == numberOfReAgeInstallments;
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate,
calculatedPrincipal.calculateValueBigDecimal(isLastInstallment),
calculatedInterest.calculateValueBigDecimal(isLastInstallment),
calculatedFees.calculateValueBigDecimal(isLastInstallment),
calculatedPenalties.calculateValueBigDecimal(isLastInstallment),
calculatedInterestAccrued.calculateValueBigDecimal(isLastInstallment),
calculatedFeeAccrued.calculateValueBigDecimal(isLastInstallment),
calculatedPenaltyAccrued.calculateValueBigDecimal(isLastInstallment));
reAgedInstallment = insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, transactionDate);
createChargeMappingsForInstallment(reAgedInstallment, calculatedCharges, isLastInstallment);
}
int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber();
List<LoanRepaymentScheduleInstallment> toRemove = installments.stream()
.filter(i -> i != null && !i.isAdditional() && i.getInstallmentNumber() != null
&& i.getInstallmentNumber() > lastReAgedInstallmentNumber && i.getTotalPaid(currency).isZero())
.toList();
toRemove.forEach(installments::remove);
reprocessInstallments(installments);
}
private void createChargeMappingsForInstallment(final LoanRepaymentScheduleInstallment installment,
List<ReAgedChargeEqualAmortizationValues> reAgedChargeEqualAmortizationValues, boolean isLastInstallment) {
reAgedChargeEqualAmortizationValues.forEach(amortizationValue -> {
installment.getInstallmentCharges()
.add(new LoanInstallmentCharge(amortizationValue.equalAmortizationValues.calculateValueBigDecimal(isLastInstallment),
amortizationValue.charge, installment));
});
}
private FirstReAgeInstallmentProps calculateFirstReAgeInstallmentProps(List<LoanRepaymentScheduleInstallment> installments,
LocalDate startDate) {
int reAgedInstallmentNumber;
LocalDate fromDate;
Optional<LoanRepaymentScheduleInstallment> lastNormalInstallmentOptional = installments.stream()
.filter(i -> !i.isDownPayment() && i.getDueDate().isBefore(startDate))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate));
if (lastNormalInstallmentOptional.isEmpty()) {
LoanRepaymentScheduleInstallment firstNormalInstallment = installments.stream().filter(i -> !i.isDownPayment())
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).orElseThrow();
reAgedInstallmentNumber = firstNormalInstallment.getInstallmentNumber();
fromDate = firstNormalInstallment.getFromDate();
} else {
LoanRepaymentScheduleInstallment lastNormalInstallment = lastNormalInstallmentOptional.get();
reAgedInstallmentNumber = lastNormalInstallment.getInstallmentNumber() + 1;
fromDate = lastNormalInstallment.getDueDate();
}
return new FirstReAgeInstallmentProps(reAgedInstallmentNumber, fromDate);
}
private void handleReAgeWithInterestRecalculationEnabled(final LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) { private void handleReAgeWithInterestRecalculationEnabled(final LoanTransaction loanTransaction, final ProgressiveTransactionCtx ctx) {
final MonetaryCurrency currency = ctx.getCurrency(); final MonetaryCurrency currency = ctx.getCurrency();
final Loan loan = loanTransaction.getLoan(); final Loan loan = loanTransaction.getLoan();
@@ -3250,6 +3550,105 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
reprocessInstallments(installments); reprocessInstallments(installments);
} }
OutstandingBalances liftOutstandingBalances(List<LoanRepaymentScheduleInstallment> installments, LocalDate transactionDate,
MonetaryCurrency currency, boolean skipDownPayments, boolean onlyPlayableInterest, boolean isEqualInstallmentForInterest,
boolean isEqualInstallmentForFeesAndPenalties, TransactionCtx ctx) {
AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency));
AtomicReference<Money> outstandingInterestBalance = new AtomicReference<>(Money.zero(currency));
AtomicReference<Money> outstandingFeesBalance = new AtomicReference<>(Money.zero(currency));
AtomicReference<Money> outstandingPenaltiesBalance = new AtomicReference<>(Money.zero(currency));
AtomicReference<Money> accruedInterestToMove = new AtomicReference<>(Money.zero(currency));
AtomicReference<Money> accruedFeeToMove = new AtomicReference<>(Money.zero(currency));
AtomicReference<Money> accruedPenaltyToMove = new AtomicReference<>(Money.zero(currency));
final List<LoanCharge> liftedLoanCharges = new ArrayList<>();
installments.stream().filter(i -> !skipDownPayments || !i.isDownPayment()).forEach(i -> {
Money principalOutstanding = i.getPrincipalOutstanding(currency);
if (principalOutstanding.isGreaterThanZero()) {
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
i.addToPrincipal(transactionDate, principalOutstanding.negated());
}
Money interestOutstanding = i.getInterestOutstanding(currency);
if (isEqualInstallmentForInterest && interestOutstanding.isGreaterThanZero()) {
outstandingInterestBalance.set(outstandingInterestBalance.get().add(interestOutstanding));
i.addToInterest(transactionDate, interestOutstanding.negated());
BigDecimal paid = MathUtil.nullToZero(i.getInterestPaid());
BigDecimal accrued = MathUtil.nullToZero(i.getInterestAccrued());
if (paid.compareTo(accrued) < 0) {
accruedInterestToMove.set(Money.of(currency, accrued.subtract(paid)));
i.setInterestAccrued(paid);
}
}
if (isEqualInstallmentForFeesAndPenalties) {
getLoanChargesOfInstallment(ctx.getCharges(), i, 1)//
.stream()//
.filter(c -> MathUtil.isGreaterThanZero(c.getAmountOutstanding()))//
.forEach(liftedLoanCharges::add);
Money feesOutstanding = i.getFeeChargesOutstanding(currency);
Money penaltiesOutstanding = i.getPenaltyChargesOutstanding(currency);
outstandingFeesBalance.set(outstandingFeesBalance.get().add(feesOutstanding));
outstandingPenaltiesBalance.set(outstandingPenaltiesBalance.get().add(penaltiesOutstanding));
i.setFeeChargesCharged(i.getFeeChargesPaid());
i.setPenaltyCharges(i.getPenaltyChargesPaid());
}
if (isEqualInstallmentForFeesAndPenalties) {
BigDecimal paid = MathUtil.nullToZero(i.getFeeChargesPaid());
BigDecimal accrued = MathUtil.nullToZero(i.getFeeAccrued());
if (paid.compareTo(accrued) < 0) {
accruedFeeToMove.set(Money.of(currency, accrued.subtract(paid)));
i.setFeeAccrued(paid);
}
}
if (isEqualInstallmentForFeesAndPenalties) {
BigDecimal paid = MathUtil.nullToZero(i.getPenaltyChargesPaid());
BigDecimal accrued = MathUtil.nullToZero(i.getPenaltyAccrued());
if (paid.compareTo(accrued) < 0) {
accruedPenaltyToMove.set(Money.of(currency, accrued.subtract(paid)));
i.setPenaltyAccrued(paid);
}
}
i.updateObligationsMet(currency, transactionDate);
});
if (isEqualInstallmentForInterest && onlyPlayableInterest) {
if (ctx instanceof ProgressiveTransactionCtx progressiveCtx) {
ProgressiveLoanInterestScheduleModel model = progressiveCtx.getModel();
outstandingInterestBalance
.set(emiCalculator.getOutstandingAmountsTillDate(model, transactionDate).getOutstandingInterest());
} else {
throw new IllegalStateException("TODO Fix me: Only progressive transaction context is supported");
}
}
return new OutstandingBalances(outstandingPrincipalBalance.get(), outstandingInterestBalance.get(), accruedInterestToMove.get(),
outstandingFeesBalance.get(), accruedFeeToMove.get(), outstandingPenaltiesBalance.get(), accruedPenaltyToMove.get(),
liftedLoanCharges);
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
private static final class CommonReAgeSettings {
boolean onlyPayableInterest = false;
boolean skipDownPayments = false;
boolean isEqualInstallmentForInterest = false;
boolean isEqualInstallmentForFeesAndPenalties = false;
}
private record ReAgedChargeEqualAmortizationValues(LoanCharge charge, EqualAmortizationValues equalAmortizationValues) {
}
private record FirstReAgeInstallmentProps(int reAgedInstallmentNumber, LocalDate fromDate) {
}
private record OutstandingBalances(Money principal, Money interest, Money interestAccrued, Money fees, Money feesAccrued,
Money penalties, Money penaltiesAccrued, List<LoanCharge> liftedLoanCharges) {
}
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanTransaction loanTransaction) { private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanTransaction loanTransaction) {
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter(); final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate(); final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
@@ -24,11 +24,14 @@ import java.math.MathContext;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
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.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -151,4 +154,18 @@ public interface EMICalculator {
*/ */
@NotNull @NotNull
Money getOutstandingInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate tillDate); Money getOutstandingInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate tillDate);
OutstandingDetails precalculateReAgeEqualAmortizationAmount(ProgressiveLoanInterestScheduleModel interestSchedule,
LocalDate transactionDate, LoanReAgeParameter reageParameter);
void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate,
LoanReAgeParameter reageParameter, Money feesPenaltiesOutstanding,
EqualAmortizationValues feesPenaltiesEqualAmortizationValues);
EqualAmortizationValues calculateEqualAmortizationValues(Money totalOutstanding, Integer numberOfInstallments,
Integer installmentAmountInMultiplesOf, MonetaryCurrency currency);
EqualAmortizationValues calculateAdjustedEqualAmortizationValues(Money outstanding, Money total,
Money sumOfOtherEqualAmortizationValues, Integer numberOfInstallments, Integer installmentAmountInMultiplesOf,
MonetaryCurrency currency);
} }
@@ -39,6 +39,7 @@ import org.apache.fineract.infrastructure.core.domain.LocalDateInterval;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData; 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.Money;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
@@ -47,11 +48,14 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
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.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; 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.EmiAdjustment;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation; import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod; import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
@@ -1584,4 +1588,185 @@ public final class ProgressiveEMICalculator implements EMICalculator {
.filter(repaymentPeriod -> originalEmi.isLessThan(repaymentPeriod.getTotalPaidAmount())) // .filter(repaymentPeriod -> originalEmi.isLessThan(repaymentPeriod.getTotalPaidAmount())) //
.count(); // .count(); //
} }
private void accelerateRepaymentDueDateTo(ProgressiveLoanInterestScheduleModel interestSchedule, RepaymentPeriod repaymentPeriod,
LocalDate transactionDate) {
repaymentPeriod.setDueDate(transactionDate);
repaymentPeriod.getInterestPeriods().getLast().setDueDate(transactionDate);
calculateRateFactorForRepaymentPeriod(repaymentPeriod, interestSchedule);
}
private void accelerateMaturityDateTo(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate) {
Optional<RepaymentPeriod> repaymentPeriod = interestSchedule.findRepaymentPeriod(transactionDate);
if (repaymentPeriod.isPresent()) {
if (!repaymentPeriod.get().getDueDate().isEqual(transactionDate)) {
accelerateRepaymentDueDateTo(interestSchedule, repaymentPeriod.get(), transactionDate);
}
if (!interestSchedule.isLastRepaymentPeriod(repaymentPeriod.get())) {
interestSchedule.repaymentPeriods().removeIf(rp -> rp.getDueDate().isAfter(transactionDate));
}
}
}
private void updateEMIForReAgeEqualAmortization(List<RepaymentPeriod> repaymentPeriods, Money principal, Money interest,
Money feesPenaltiesOutstanding, EqualAmortizationValues feesPenaltiesEqualAmortizationValues, MonetaryCurrency currency) {
EqualAmortizationValues interestEAV = calculateEqualAmortizationValues(interest, repaymentPeriods.size(), null, currency);
EqualAmortizationValues principalEAV = calculateAdjustedEqualAmortizationValues(principal,
principal.add(interest).add(feesPenaltiesOutstanding),
interestEAV.value().add(feesPenaltiesEqualAmortizationValues.value()), repaymentPeriods.size(), null, currency);
RepaymentPeriod last = repaymentPeriods.getLast();
EqualAmortizationValues emiAEV = principalEAV.add(interestEAV);
repaymentPeriods.forEach(rp -> {
boolean isLast = last.equals(rp);
rp.setReAgedInterest(interestEAV.calculateValue(isLast));
Money emi = emiAEV.calculateValue(isLast);
rp.setEmi(emi);
rp.setOriginalEmi(emi);
});
}
@Override
public OutstandingDetails precalculateReAgeEqualAmortizationAmount(ProgressiveLoanInterestScheduleModel interestSchedule,
LocalDate transactionDate, LoanReAgeParameter reageParameter) {
return getOutstandingAmountsTillDate(interestSchedule,
reageParameter.getInterestHandlingType().equals(LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST)
? transactionDate
: interestSchedule.getMaturityDate());
}
@Override
public void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate,
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);
// calculate already paid balances from transaction date
OutstandingDetails paidBalancesFromTransactionDate = calculatePaidBalancesAfterDate(interestSchedule, transactionDate);
// set maturity date to transaction date and remove all repayment periods after it.
accelerateMaturityDateTo(interestSchedule, transactionDate);
// 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);
});
// stop calculate unrecognised interest at this point because all
interestSchedule.getLastRepaymentPeriod().setNoUnrecognisedInterest(true);
if (!paidBalancesFromTransactionDate.getOutstandingInterest().isZero()
|| !paidBalancesFromTransactionDate.getOutstandingPrincipal().isZero()) {
createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(interestSchedule,
paidBalancesFromTransactionDate.getOutstandingPrincipal(), paidBalancesFromTransactionDate.getOutstandingInterest());
}
updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods, isAfterOriginalMaturityDate);
updateEMIForReAgeEqualAmortization(reAgedRepaymentPeriods, reAgeingAmounts.getOutstandingPrincipal(),
reAgeingAmounts.getOutstandingInterest(), feesPenaltiesOutstanding, feesPenaltiesEqualAmortizationValues,
interestSchedule.zero().getCurrency());
calculateOutstandingBalance(interestSchedule);
LocalDate zeroInterestFrom = DateUtils.min(transactionDate, originalMaturityDate);
interestSchedule.addInterestRate(zeroInterestFrom, BigDecimal.ZERO);
calculateLastUnpaidRepaymentPeriodEMI(interestSchedule, transactionDate);
}
private void updateModelForReageEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule,
LoanReAgeParameter reageParameter, List<RepaymentPeriod> reAgedRepaymentPeriods, boolean isAfterOriginalMaturityDate) {
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());
repaymentPeriod.setTotalCapitalizedIncomeAmount(previous.getTotalCapitalizedIncomeAmount());
repaymentPeriod.setTotalDisbursedAmount(previous.getTotalDisbursedAmount());
repaymentPeriod.setReAged(true);
interestSchedule.repaymentPeriods().add(repaymentPeriod);
reAgedRepaymentPeriods.add(repaymentPeriod);
previous = repaymentPeriod;
toDate = scheduledDateGenerator.getRepaymentPeriodDate(frequencyType, frequency, toDate);
}
}
private void createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(ProgressiveLoanInterestScheduleModel interestSchedule,
Money totalPaidPrincipal, Money totalPaidInterest) {
RepaymentPeriod targetPeriod = interestSchedule.getLastRepaymentPeriod();
Money paidInterestToAdd = totalPaidInterest.minus(targetPeriod.getPaidInterest());
Money paidPrincipalToAdd = totalPaidPrincipal.minus(targetPeriod.getPaidPrincipal());
targetPeriod.addPaidInterestAmount(paidInterestToAdd);
targetPeriod.addPaidPrincipalAmount(paidPrincipalToAdd);
targetPeriod.setEmi(targetPeriod.getTotalPaidAmount());
targetPeriod.setReAged(true);
targetPeriod.setReAgedEarlyRepaymentHolder(true);
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))
.map(RepaymentPeriod::getPaidPrincipal).reduce(interestSchedule.zero(), Money::add);
Money interest = interestSchedule.repaymentPeriods().stream().filter(rp -> !rp.getDueDate().isBefore(transactionDate))
.map(RepaymentPeriod::getPaidInterest).reduce(interestSchedule.zero(), Money::add);
return new OutstandingDetails(principal, interest);
}
@Override
public EqualAmortizationValues calculateEqualAmortizationValues(Money totalOutstanding, Integer numberOfInstallments,
Integer installmentAmountInMultiplesOf, MonetaryCurrency currency) {
if (totalOutstanding.isGreaterThanZero()) {
Money equalMonthlyValue = totalOutstanding.dividedBy(numberOfInstallments, totalOutstanding.getMc());
if (installmentAmountInMultiplesOf != null) {
equalMonthlyValue = Money.roundToMultiplesOf(equalMonthlyValue, installmentAmountInMultiplesOf);
}
Money adjustmentForLastInstallment = totalOutstanding.minus(equalMonthlyValue.multipliedBy(numberOfInstallments));
return new EqualAmortizationValues(equalMonthlyValue, adjustmentForLastInstallment);
}
return new EqualAmortizationValues(Money.zero(currency), Money.zero(currency));
}
@Override
public EqualAmortizationValues calculateAdjustedEqualAmortizationValues(Money outstanding, Money total,
Money sumOfOtherEqualAmortizationValues, Integer numberOfInstallments, Integer installmentAmountInMultiplesOf,
MonetaryCurrency currency) {
EqualAmortizationValues calculatedEMI = calculateEqualAmortizationValues(total, numberOfInstallments,
installmentAmountInMultiplesOf, currency);
Money value = calculatedEMI.value().minus(sumOfOtherEqualAmortizationValues);
Money adjust = outstanding.minus(value.multipliedBy(numberOfInstallments));
return new EqualAmortizationValues(value, adjust);
}
} }
@@ -0,0 +1,41 @@
/**
* 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.loanproduct.calc.data;
import java.math.BigDecimal;
import org.apache.fineract.organisation.monetary.domain.Money;
public record EqualAmortizationValues(Money value, Money adjustment) {
public Money getAdjustedValue() {
return value.add(adjustment);
}
public Money calculateValue(boolean isLast) {
return (isLast ? getAdjustedValue() : value);
}
public BigDecimal calculateValueBigDecimal(boolean isLast) {
return calculateValue(isLast).getAmount();
}
public EqualAmortizationValues add(EqualAmortizationValues other) {
return new EqualAmortizationValues(value.add(other.value), adjustment.add(other.adjustment));
}
}
@@ -138,7 +138,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
} }
public BigDecimal getCalculatedDueInterest() { public BigDecimal getCalculatedDueInterest() {
if (isPaused()) { if (isPaused() || getRepaymentPeriod().isReAged()) {
return getCreditedInterest().getAmount(); return getCreditedInterest().getAmount();
} }
@@ -92,9 +92,23 @@ public class RepaymentPeriod {
@Setter @Setter
private boolean isOutstandingMovedDueToReAging = false; private boolean isOutstandingMovedDueToReAging = false;
@Setter
@Getter
private boolean noUnrecognisedInterest;
@Setter
@Getter
private boolean reAged = false;
@Setter
@Getter
private boolean reAgedEarlyRepaymentHolder = false;
@Getter
@Setter
private Money reAgedInterest;
protected RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods, protected RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, List<InterestPeriod> interestPeriods,
Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc, Money emi, Money originalEmi, Money paidPrincipal, Money paidInterest, Money futureUnrecognizedInterest, MathContext mc,
ILoanConfigurationDetails loanProductRelatedDetail) { ILoanConfigurationDetails loanProductRelatedDetail, boolean noUnrecognisedInterest, boolean reAged,
boolean reAgedEarlyRepaymentHolder, Money reAgedInterest) {
this.previous = previous; this.previous = previous;
this.fromDate = fromDate; this.fromDate = fromDate;
this.dueDate = dueDate; this.dueDate = dueDate;
@@ -106,17 +120,22 @@ public class RepaymentPeriod {
this.futureUnrecognizedInterest = futureUnrecognizedInterest; this.futureUnrecognizedInterest = futureUnrecognizedInterest;
this.mc = mc; this.mc = mc;
this.loanProductRelatedDetail = loanProductRelatedDetail; this.loanProductRelatedDetail = loanProductRelatedDetail;
this.noUnrecognisedInterest = noUnrecognisedInterest;
this.reAged = reAged;
this.reAgedEarlyRepaymentHolder = reAgedEarlyRepaymentHolder;
this.reAgedInterest = reAgedInterest;
} }
public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc, ILoanConfigurationDetails loanProductRelatedDetail) { public static RepaymentPeriod empty(RepaymentPeriod previous, MathContext mc, ILoanConfigurationDetails loanProductRelatedDetail) {
return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc, loanProductRelatedDetail); return new RepaymentPeriod(previous, null, null, new ArrayList<>(), null, null, null, null, null, mc, loanProductRelatedDetail,
false, false, false, null);
} }
public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc, public static RepaymentPeriod create(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi, MathContext mc,
ILoanConfigurationDetails loanProductRelatedDetail) { ILoanConfigurationDetails loanProductRelatedDetail) {
final Money zero = emi.zero(); final Money zero = emi.zero();
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero, final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, fromDate, dueDate, new ArrayList<>(), emi, emi, zero, zero,
zero, mc, loanProductRelatedDetail); zero, mc, loanProductRelatedDetail, false, false, false, zero);
// There is always at least 1 interest period, by default with same from-due date as repayment period // There is always at least 1 interest period, by default with same from-due date as repayment period
newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate)); newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate));
return newRepaymentPeriod; return newRepaymentPeriod;
@@ -126,7 +145,8 @@ public class RepaymentPeriod {
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(), final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(),
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(),
repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc, repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc,
repaymentPeriod.getLoanProductRelatedDetail()); repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(),
repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging()); newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging());
// There is always at least 1 interest period, by default with same from-due date as repayment period // There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) { for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
@@ -139,7 +159,8 @@ public class RepaymentPeriod {
final Money zero = Money.zero(repaymentPeriod.getCurrency(), mc); final Money zero = Money.zero(repaymentPeriod.getCurrency(), mc);
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(), final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(),
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero, repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(), zero, zero,
zero, mc, repaymentPeriod.getLoanProductRelatedDetail()); zero, mc, repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(),
repaymentPeriod.isReAged(), repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging()); newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging());
// There is always at least 1 interest period, by default with same from-due date as repayment period // There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) { for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
@@ -180,8 +201,8 @@ public class RepaymentPeriod {
@NotNull @NotNull
public Money getCalculatedDueInterest() { public Money getCalculatedDueInterest() {
if (calculatedDueInterestCalculation == null) { if (calculatedDueInterestCalculation == null) {
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { this.previous, calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { previous, interestPeriods,
this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved, this.totalDisbursedAmount }); futureUnrecognizedInterest, isInterestMoved, totalDisbursedAmount, reAgedInterest, reAged });
} }
return calculatedDueInterestCalculation.get(); return calculatedDueInterestCalculation.get();
} }
@@ -193,6 +214,7 @@ public class RepaymentPeriod {
getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(BigDecimal.ZERO, BigDecimal::add), getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(BigDecimal.ZERO, BigDecimal::add),
mc); mc);
} }
calculatedDueInterest = calculatedDueInterest.add(reAgedInterest);
calculatedDueInterest = calculatedDueInterest.add(getFutureUnrecognizedInterest(), getMc()); calculatedDueInterest = calculatedDueInterest.add(getFutureUnrecognizedInterest(), getMc());
if (getPrevious().isPresent()) { if (getPrevious().isPresent()) {
calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest(), getMc()); calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest(), getMc());
@@ -213,7 +235,7 @@ public class RepaymentPeriod {
: MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false), : MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false),
getPaidInterest(), false), getPaidInterest(), false),
() -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest, totalDisbursedAmount, () -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest, totalDisbursedAmount,
emi }); reAgedInterest, reAged, emi });
} }
return dueInterestCalculation.get(); return dueInterestCalculation.get();
} }
@@ -312,7 +334,7 @@ public class RepaymentPeriod {
* @return * @return
*/ */
public Money getUnrecognizedInterest() { public Money getUnrecognizedInterest() {
return getCalculatedDueInterest().minus(getDueInterest(), getMc()); return noUnrecognisedInterest ? getZero() : getCalculatedDueInterest().minus(getDueInterest(), getMc());
} }
public Money getCreditedAmounts() { public Money getCreditedAmounts() {
@@ -0,0 +1,33 @@
/**
* 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.util;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
public final class InstallmentProcessingHelper {
private InstallmentProcessingHelper() {}
public static void addOneToInstallmentNumberFromInstallment(final List<LoanRepaymentScheduleInstallment> installments,
final int installmentNumber) {
installments.stream().filter(i -> i.getInstallmentNumber() != null && i.getInstallmentNumber() >= installmentNumber)
.forEach(i -> i.setInstallmentNumber(i.getInstallmentNumber() + 1));
}
}
@@ -71,6 +71,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
@@ -120,7 +121,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void setUp() { public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(InterestRefundService.class), underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(InterestRefundService.class),
Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class), Mockito.mock(LoanChargeValidator.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class), Mockito.mock(LoanChargeValidator.class),
Mockito.mock(LoanBalanceService.class), Mockito.mock(LoanChargeService.class)); Mockito.mock(LoanBalanceService.class), Mockito.mock(LoanChargeService.class), Mockito.mock(ScheduledDateGenerator.class));
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
@@ -490,7 +491,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
// Set up TransactionCtx with installments and charges // Set up TransactionCtx with installments and charges
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, Set.of(), overpaymentHolder, changedTransactionDetail, TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, Set.of(), overpaymentHolder, changedTransactionDetail,
model); model, null);
// Mock additional necessary methods // Mock additional necessary methods
LoanCharge loanCharge = mock(LoanCharge.class); LoanCharge loanCharge = mock(LoanCharge.class);
@@ -18,7 +18,7 @@
*/ */
package org.apache.fineract.portfolio.loanproduct.calc; package org.apache.fineract.portfolio.loanproduct.calc;
import static org.mockito.Mockito.mock; import static java.math.BigDecimal.ZERO;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
@@ -40,10 +40,16 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; 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.loanaccount.service.ProgressiveLoanInterestScheduleModelParserServiceGsonImpl;
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod; import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod; import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
@@ -67,7 +73,7 @@ import org.springframework.lang.NonNull;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class ProgressiveEMICalculatorTest { class ProgressiveEMICalculatorTest {
private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class)); private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(new DefaultScheduledDateGenerator());
private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class); private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class); private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
@@ -4174,6 +4180,582 @@ class ProgressiveEMICalculatorTest {
} }
@Nested
public class ReAgeEqualAmortization {
@Test
public void test_transactionInMiddleOfPeriod_EQUAL_AMORTIZATION_FULL_INTEREST_noTransactionTilDate_noInterestRecalc() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(15.678);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.43, 0.0, 0.0, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 0, 1, 17.43, 0.013315561644, 1.3315561644, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 1, 0, 17.43, 0.012456493151, 1.04509977537, 1.05, 16.38, 67.52);
checkPeriod(interestSchedule, 2, 0, 17.43, 0.013315561644, 0.899066722202, 0.90, 16.53, 50.99);
checkPeriod(interestSchedule, 3, 0, 17.43, 0.012886027397, 0.657058536972, 0.66, 16.77, 34.22);
checkPeriod(interestSchedule, 4, 0, 17.43, 0.013315561644, 0.455658519458, 0.46, 16.97, 17.25);
checkPeriod(interestSchedule, 5, 0, 17.47, 0.012886027397, 0.222283972598, 0.22, 17.25, 0.0);
// No repayment no interest recalculation
LocalDate reAgingStartDate = LocalDate.of(2024, 4, 20);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
LoanTransaction loanTransaction = new LoanTransaction(null, null, LoanTransactionType.REAGE, LocalDate.of(2024, 4, 15),
outstandingAmountsTillDate.getOutstandingPrincipal().add(outstandingAmountsTillDate.getOutstandingInterest())
.getAmount(),
outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDate.getOutstandingInterest().getAmount(), ZERO, ZERO, ZERO, false, null, null);
LoanReAgeParameter reageParameter = new LoanReAgeParameter(loanTransaction, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null);
loanTransaction.setLoanReAgeParameter(reageParameter);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, loanTransaction.getTransactionDate(), reageParameter,
Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_transactionInMiddleOfPeriod_EQUAL_AMORTIZATION_FULL_INTEREST() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(15.678);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.43, 0.0, 0.0, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 0, 1, 17.43, 0.013315561644, 1.3315561644, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 1, 0, 17.43, 0.012456493151, 1.04509977537, 1.05, 16.38, 67.52);
checkPeriod(interestSchedule, 2, 0, 17.43, 0.013315561644, 0.899066722202, 0.90, 16.53, 50.99);
checkPeriod(interestSchedule, 3, 0, 17.43, 0.012886027397, 0.657058536972, 0.66, 16.77, 34.22);
checkPeriod(interestSchedule, 4, 0, 17.43, 0.013315561644, 0.455658519458, 0.46, 16.97, 17.25);
checkPeriod(interestSchedule, 5, 0, 17.47, 0.012886027397, 0.222283972598, 0.22, 17.25, 0.0);
// repay EMI on first period due date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.1)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(1.33)));
// repay EMI on first period due date - next installment for 2nd period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
// repay 20 for last installment on first period due date -- ie MIR
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(2.57)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 1),
emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 4, 1)).getDuePrincipal());
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 15), emiCalculator
.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 4, 1)).getDuePrincipal().negated());
LocalDate reAgingStartDate = LocalDate.of(2024, 4, 20);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
LoanTransaction loanTransaction = new LoanTransaction(null, null, LoanTransactionType.REAGE, LocalDate.of(2024, 4, 15),
outstandingAmountsTillDate.getOutstandingPrincipal().add(outstandingAmountsTillDate.getOutstandingInterest())
.getAmount(),
outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDate.getOutstandingInterest().getAmount(), ZERO, ZERO, ZERO, false, null, null);
LoanReAgeParameter reageParameter = new LoanReAgeParameter(loanTransaction, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null);
loanTransaction.setLoanReAgeParameter(reageParameter);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, loanTransaction.getTransactionDate(), reageParameter,
Money.zero(currency), new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_chargeback_transactionInMiddleOfPeriod_EQUAL_EQUAL_AMORTIZATION_PAYABLE_INTEREST() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
// repay EMI on first period due date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.43)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(0.58)));
// chargeback
emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 3, 15), Money.of(currency, BigDecimal.valueOf(16.43)));
emiCalculator.creditInterest(interestSchedule, LocalDate.of(2024, 3, 15), Money.of(currency, BigDecimal.valueOf(0.58)));
LocalDate transactionDate = LocalDate.of(2024, 3, 15);
LocalDate reAgingStartDate = LocalDate.of(2024, 4, 1);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, transactionDate);
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_chargebackPartial_transactionInMiddleOfPeriod_EQUAL_EQUAL_AMORTIZATION_FULL_INTEREST() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(7.0);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
// repay EMI on first period due date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.43)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(0.58)));
// chargeback
emiCalculator.creditPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), Money.of(currency, BigDecimal.valueOf(9.42)));
emiCalculator.creditInterest(interestSchedule, LocalDate.of(2024, 2, 1), Money.of(currency, BigDecimal.valueOf(0.58)));
LocalDate transactionDate = LocalDate.of(2024, 3, 15);
LocalDate reAgingStartDate = LocalDate.of(2024, 4, 1);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_transactionInMiddleOfPeriod_EQUAL_EQUAL_AMORTIZATION_PAYABLE_INTEREST() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(15.678);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.43, 0.0, 0.0, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 0, 1, 17.43, 0.013315561644, 1.3315561644, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 1, 0, 17.43, 0.012456493151, 1.04509977537, 1.05, 16.38, 67.52);
checkPeriod(interestSchedule, 2, 0, 17.43, 0.013315561644, 0.899066722202, 0.90, 16.53, 50.99);
checkPeriod(interestSchedule, 3, 0, 17.43, 0.012886027397, 0.657058536972, 0.66, 16.77, 34.22);
checkPeriod(interestSchedule, 4, 0, 17.43, 0.013315561644, 0.455658519458, 0.46, 16.97, 17.25);
checkPeriod(interestSchedule, 5, 0, 17.47, 0.012886027397, 0.222283972598, 0.22, 17.25, 0.0);
// repay EMI on first period due date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.1)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(1.33)));
// repay EMI on first period due date - next installment for 2nd period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
// repay 20 for last installment on first period due date -- ie MIR
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(2.57)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 1),
emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 4, 1)).getDuePrincipal());
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 15), emiCalculator
.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 4, 1)).getDuePrincipal().negated());
LocalDate transactionDate = LocalDate.of(2024, 4, 15);
LocalDate reAgingStartDate = LocalDate.of(2024, 4, 20);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, transactionDate);
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_transactionInMiddleOfPeriod_stringOnNextDueDate_EQUAL_EQUAL_AMORTIZATION_PAYABLE_INTEREST() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(15.678);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.43, 0.0, 0.0, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 0, 1, 17.43, 0.013315561644, 1.3315561644, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 1, 0, 17.43, 0.012456493151, 1.04509977537, 1.05, 16.38, 67.52);
checkPeriod(interestSchedule, 2, 0, 17.43, 0.013315561644, 0.899066722202, 0.90, 16.53, 50.99);
checkPeriod(interestSchedule, 3, 0, 17.43, 0.012886027397, 0.657058536972, 0.66, 16.77, 34.22);
checkPeriod(interestSchedule, 4, 0, 17.43, 0.013315561644, 0.455658519458, 0.46, 16.97, 17.25);
checkPeriod(interestSchedule, 5, 0, 17.47, 0.012886027397, 0.222283972598, 0.22, 17.25, 0.0);
// repay EMI on first period due date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.1)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(1.33)));
// repay EMI on first period due date - next installment for 2nd period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
// repay 20 for last installment on first period due date -- ie MIR
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(2.57)));
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 1),
emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 4, 1)).getDuePrincipal());
emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 15), emiCalculator
.getDueAmounts(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 4, 1)).getDuePrincipal().negated());
LocalDate transactionDate = LocalDate.of(2024, 4, 15);
LocalDate reAgingStartDate = LocalDate.of(2024, 5, 1);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, transactionDate);
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_transactionOnMaturityDate_stringAfterMaturityDate_EQUAL_EQUAL_AMORTIZATION_PAYABLE_INTEREST() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(15.678);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.43, 0.0, 0.0, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 0, 1, 17.43, 0.013315561644, 1.3315561644, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 1, 0, 17.43, 0.012456493151, 1.04509977537, 1.05, 16.38, 67.52);
checkPeriod(interestSchedule, 2, 0, 17.43, 0.013315561644, 0.899066722202, 0.90, 16.53, 50.99);
checkPeriod(interestSchedule, 3, 0, 17.43, 0.012886027397, 0.657058536972, 0.66, 16.77, 34.22);
checkPeriod(interestSchedule, 4, 0, 17.43, 0.013315561644, 0.455658519458, 0.46, 16.97, 17.25);
checkPeriod(interestSchedule, 5, 0, 17.47, 0.012886027397, 0.222283972598, 0.22, 17.25, 0.0);
// repay EMI on first period due date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.1)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(1.33)));
// repay EMI on first period due date - next installment for 2nd period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
// repay 20 for last installment on first period due date -- ie MIR
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(2.57)));
LocalDate transactionDate = LocalDate.of(2024, 7, 1);
LocalDate reAgingStartDate = LocalDate.of(2024, 7, 15);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, transactionDate);
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
@Test
public void test_transactionAfterMaturityDate_EQUAL_EQUAL_AMORTIZATION_PAYABLE_INTEREST() {
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)));
expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)));
expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)));
expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)));
expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)));
final BigDecimal interestRate = BigDecimal.valueOf(15.678);
final Integer installmentAmountInMultiplesOf = null;
Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate);
Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_365.getValue());
Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue());
Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS);
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency);
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc);
final Money disbursedAmount = toMoney(100.0);
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
checkPeriod(interestSchedule, 0, 0, 17.43, 0.0, 0.0, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 0, 1, 17.43, 0.013315561644, 1.3315561644, 1.33, 16.1, 83.9);
checkPeriod(interestSchedule, 1, 0, 17.43, 0.012456493151, 1.04509977537, 1.05, 16.38, 67.52);
checkPeriod(interestSchedule, 2, 0, 17.43, 0.013315561644, 0.899066722202, 0.90, 16.53, 50.99);
checkPeriod(interestSchedule, 3, 0, 17.43, 0.012886027397, 0.657058536972, 0.66, 16.77, 34.22);
checkPeriod(interestSchedule, 4, 0, 17.43, 0.013315561644, 0.455658519458, 0.46, 16.97, 17.25);
checkPeriod(interestSchedule, 5, 0, 17.47, 0.012886027397, 0.222283972598, 0.22, 17.25, 0.0);
// repay EMI on first period due date
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(16.1)));
emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(1.33)));
// repay EMI on first period due date - next installment for 2nd period
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
// repay 20 for last installment on first period due date -- ie MIR
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(17.43)));
emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 1),
Money.of(currency, BigDecimal.valueOf(2.57)));
LocalDate transactionDate = LocalDate.of(2024, 7, 10);
LocalDate reAgingStartDate = LocalDate.of(2024, 7, 15);
OutstandingDetails outstandingAmountsTillDate = emiCalculator.getOutstandingAmountsTillDate(interestSchedule, transactionDate);
LoanReAgeParameter reageParameter = new LoanReAgeParameter(null, PeriodFrequencyType.MONTHS, 1, reAgingStartDate, 6,
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST, null);
// Update the existing model with re-aged periods
emiCalculator.reAgeEqualAmortization(interestSchedule, transactionDate, reageParameter, Money.zero(currency),
new EqualAmortizationValues(Money.zero(currency), Money.zero(currency)));
OutstandingDetails outstandingAmountsTillDateAfterReage = emiCalculator.getOutstandingAmountsTillDate(interestSchedule,
interestSchedule.getMaturityDate());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingInterest().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingInterest().getAmount());
Assertions.assertEquals(outstandingAmountsTillDate.getOutstandingPrincipal().getAmount(),
outstandingAmountsTillDateAfterReage.getOutstandingPrincipal().getAmount());
}
}
// utilities // utilities
private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) { private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) {
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) { return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
@@ -122,7 +122,12 @@ class RepaymentPeriodTest {
null, // paidInterest null, // paidInterest
null, // futureUnrecognizedInterest null, // futureUnrecognizedInterest
MC, // mc MC, // mc
loanProductRelatedDetail); loanProductRelatedDetail, //
false, // noUnrecognizedInterest
false, // reAged
false, // reAgedEarlyRepaymentHolder
null // reAgedInterest
);
// Test that getters don't throw and return non-null // Test that getters don't throw and return non-null
assertDoesNotThrow(period::getEmi); assertDoesNotThrow(period::getEmi);
@@ -35,7 +35,6 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData; import org.apache.fineract.portfolio.loanaccount.data.TransactionChangeData;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
@@ -62,8 +61,19 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
private static void simulateRepaymentForDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal, private static void simulateRepaymentForDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal,
List<LoanTransaction> collect) { List<LoanTransaction> collect) {
collect.add(new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), lt.getTypeOf(), lt.getDateOf(), lt.getAmount(), LoanTransaction copy = new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), lt.getTypeOf(), lt.getDateOf(), lt.getAmount(),
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, null)); BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, null);
if (LoanTransactionType.CHARGE_PAYMENT.equals(copy.getTypeOf())
|| LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.equals(copy.getTypeOf())) {
copy.getLoanChargesPaid().addAll(copy.getLoanChargesPaid());
}
if (LoanTransactionType.REAGE.equals(copy.getTypeOf())) {
copy.setLoanReAgeParameter(lt.getLoanReAgeParameter().getCopy(copy));
}
if (LoanTransactionType.REAMORTIZE.equals(copy.getTypeOf())) {
copy.setLoanReAmortizationParameter(lt.getLoanReAmortizationParameter());
}
collect.add(copy);
if (lt.getTypeOf().isDisbursement() && MathUtil.isGreaterThanZero(refundFinal.get())) { if (lt.getTypeOf().isDisbursement() && MathUtil.isGreaterThanZero(refundFinal.get())) {
if (lt.getAmount().compareTo(refundFinal.get()) <= 0) { if (lt.getAmount().compareTo(refundFinal.get()) <= 0) {
collect.add(new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), REPAYMENT, lt.getDateOf(), lt.getAmount(), collect.add(new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), REPAYMENT, lt.getDateOf(), lt.getAmount(),
@@ -79,15 +89,14 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
private Money recalculateTotalInterest(AdvancedPaymentScheduleTransactionProcessor processor, Loan loan, private Money recalculateTotalInterest(AdvancedPaymentScheduleTransactionProcessor processor, Loan loan,
LocalDate relatedRefundTransactionDate, List<LoanTransaction> transactionsToReprocess) { LocalDate relatedRefundTransactionDate, List<LoanTransaction> transactionsToReprocess) {
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>(
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess, .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges()); loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
final List<LoanTransaction> newTransactions = reprocessResult.getLeft().getTransactionChanges().stream() final List<LoanTransaction> newTransactions = reprocessResult.getLeft().getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed).toList(); .map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed).toList();
loan.getLoanTransactions().addAll(newTransactions); loan.getLoanTransactions().addAll(newTransactions);
@@ -196,7 +196,11 @@ public class LoanReAgingService {
} }
private void processReAgeTransaction(final Loan loan, final LoanTransaction reAgeTransaction, final boolean withPostTransactionChecks) { private void processReAgeTransaction(final Loan loan, final LoanTransaction reAgeTransaction, final boolean withPostTransactionChecks) {
if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())) { if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())
|| LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST
.equals(reAgeTransaction.getLoanReAgeParameter().getInterestHandlingType())
|| LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST
.equals(reAgeTransaction.getLoanReAgeParameter().getInterestHandlingType())) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null); final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
if (withPostTransactionChecks) { if (withPostTransactionChecks) {
@@ -35,6 +35,7 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest; import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
@@ -113,6 +114,13 @@ public class LoanReAgingValidator {
} }
private void validateReAgeBusinessRules(Loan loan) { private void validateReAgeBusinessRules(Loan loan) {
// validate reaging shouldn't happen before maturity
// on progressive loans it can
if (!loan.isProgressiveSchedule() && DateUtils.isBefore(getBusinessLocalDate(), loan.getMaturityDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.cannot.be.submitted.before.maturity",
"Loan cannot be re-aged before maturity", loan.getId());
}
// validate reaging is only available for progressive schedule & advanced payment allocation // validate reaging is only available for progressive schedule & advanced payment allocation
LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loan.getLoanProductRelatedDetail().getLoanScheduleType().name()); LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loan.getLoanProductRelatedDetail().getLoanScheduleType().name());
boolean isProgressiveSchedule = LoanScheduleType.PROGRESSIVE.equals(loanScheduleType); boolean isProgressiveSchedule = LoanScheduleType.PROGRESSIVE.equals(loanScheduleType);
@@ -32,6 +32,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
@@ -137,8 +138,8 @@ public class LoanAccountAutoStarter {
final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService,
final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule, final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService, final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService,
@Lazy final LoanChargeService loanChargeService) { @Lazy final LoanChargeService loanChargeService, final ScheduledDateGenerator scheduledDateGenerator) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, progressiveLoanInterestRefundService, externalIdFactory, return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, progressiveLoanInterestRefundService, externalIdFactory,
loanSchedule, loanChargeValidator, loanBalanceService, loanChargeService); loanSchedule, loanChargeValidator, loanBalanceService, loanChargeService, scheduledDateGenerator);
} }
} }