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);
}
}
/**
* 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,
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal) {
return new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal, null, null, null, null, null,
null, null, false, false, true);
final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principal, final BigDecimal interest, final BigDecimal fees,
final BigDecimal penalties, final BigDecimal interestAccrued, final BigDecimal feeAccrued, final BigDecimal penaltyAccrued) {
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) {
@@ -43,6 +43,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -57,6 +58,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
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.LoanTransactionToRepaymentScheduleMapping;
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.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
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.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanConfigurationDetailsMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
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.schedule.LoanScheduleComponent;
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.PeriodDueDetails;
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.LoanProductRelatedDetail;
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.LoopGuard;
@@ -129,17 +135,19 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private final LoanScheduleComponent loanSchedule;
private final LoanChargeService loanChargeService;
private final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeRepaymentScheduleProcessing;
private final ScheduledDateGenerator scheduledDateGenerator;
public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final InterestRefundService interestRefundService,
final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService,
final LoanChargeService loanChargeService) {
final LoanChargeService loanChargeService, ScheduledDateGenerator scheduledDateGenerator) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
this.emiCalculator = emiCalculator;
this.interestRefundService = interestRefundService;
this.loanSchedule = loanSchedule;
this.loanChargeService = loanChargeService;
this.loanChargeRepaymentScheduleProcessing = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
this.scheduledDateGenerator = scheduledDateGenerator;
}
@Override
@@ -466,15 +474,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
} else {
if (ctx instanceof ProgressiveTransactionCtx progCtx) {
final Money interestBeforeRefund = emiCalculator.getSumOfDueInterestsOnDate(progCtx.getModel(),
loanTransaction.getDateOf());
LocalDate targetDate = loanTransaction.getDateOf();
final Money interestBeforeRefund = emiCalculator.getSumOfDueInterestsOnDate(progCtx.getModel(), targetDate);
final List<Long> unmodifiedTransactionIds = progCtx.getAlreadyProcessedTransactions().stream()
.filter(LoanTransaction::isNotReversed).map(AbstractPersistableCustom::getId).toList();
final List<LoanTransaction> modifiedTransactions = new ArrayList<>(progCtx.getAlreadyProcessedTransactions().stream()
.filter(LoanTransaction::isNotReversed).filter(tr -> tr.getId() == null).toList());
if (!modifiedTransactions.isEmpty()) {
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(),
loanTransaction.getDateOf(), modifiedTransactions, unmodifiedTransactionIds);
final Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loan.getId(), targetDate,
modifiedTransactions, unmodifiedTransactionIds);
final Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
loanTransaction.updateAmount(newAmount.getAmount());
}
@@ -2291,8 +2299,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
HorizontalPaymentAllocationContext paymentAllocationContext = new HorizontalPaymentAllocationContext(ctx, loanTransaction,
paymentAllocationTypes, futureInstallmentAllocationRule, transactionMappings, balances);
paymentAllocationContext.setTransactionAmountUnprocessed(transactionAmountUnprocessed);
boolean interestBearingAndInterestRecalculationEnabled = loanTransaction.getLoan()
.isInterestBearingAndInterestRecalculationEnabled();
if (isInterestRecalculationSupported(ctx, loanTransaction.getLoan())) {
// Clear any previously skipped installments before re-evaluating
@@ -2592,6 +2598,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
final boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber);
return charges.stream()
.filter(loanCharge -> (loanCharge.isInstalmentFee() && loanCharge.hasInstallmentFor(installment))
|| (installment.isReAged() && loanCharge.hasInstallmentFor(installment))
|| loanCharge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment))
.collect(Collectors.toSet());
}
@@ -2760,6 +2767,39 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
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,
final LoanRepaymentScheduleInstallment reAgedInstallment, MonetaryCurrency currency, LocalDate transactionDate) {
target.setAdditional(false);
@@ -2767,11 +2807,19 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
target.setFromDate(reAgedInstallment.getFromDate());
target.setDueDate(reAgedInstallment.getDueDate());
target.setPrincipal(MathUtil.add(reAgedInstallment.getPrincipal(), target.getPrincipalCompleted()));
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);
}
private void insertOrReplaceRelatedInstallment(List<LoanRepaymentScheduleInstallment> installments,
private LoanRepaymentScheduleInstallment insertOrReplaceRelatedInstallment(List<LoanRepaymentScheduleInstallment> installments,
final LoanRepaymentScheduleInstallment reAgedInstallment, final MonetaryCurrency currency, final LocalDate transactionDate) {
Optional<LoanRepaymentScheduleInstallment> first = installments.stream()
.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
if (!target.getDueDate().isAfter(reAgedInstallment.getDueDate())) {
mergeReAgedInstallment(target, reAgedInstallment, currency, transactionDate);
return target;
} else {
InstallmentProcessingHelper.addOneToInstallmentNumberFromInstallment(installments,
reAgedInstallment.getInstallmentNumber());
installments.add(reAgedInstallment);
reAgedInstallment.updateObligationsMet(currency, transactionDate);
installments.stream()
.filter(i -> i.getInstallmentNumber() != null && i.getInstallmentNumber() >= target.getInstallmentNumber())
.forEach(i -> i.setInstallmentNumber(i.getInstallmentNumber() + 1));
return reAgedInstallment;
}
} else {
mergeReAgedInstallment(target, reAgedInstallment, currency, transactionDate);
return target;
}
} else {
installments.add(reAgedInstallment);
reAgedInstallment.updateObligationsMet(currency, transactionDate);
return reAgedInstallment;
}
}
private void handleReAge(LoanTransaction loanTransaction, TransactionCtx ctx) {
loanTransaction.resetDerivedComponents();
MonetaryCurrency currency = ctx.getCurrency();
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
Loan loan = loanTransaction.getLoan();
LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
// re-aging logic for interest-bearing loans
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
handleReAgeWithInterestRecalculationEnabled(loanTransaction, progressiveTransactionCtx);
} else if (loanTransaction.getLoan().isInterestBearing() && !loanTransaction.getLoan().isInterestRecalculationEnabled()) {
// TODO: implement interestRecalculation = false logic
throw new NotImplementedException(
"Logic for re-aging when interest bearing loan has interestRecalculation disabled is not implemented");
} else {
AtomicReference<Money> outstandingPrincipalBalance = new AtomicReference<>(Money.zero(currency));
installments.forEach(i -> {
Money principalOutstanding = i.getPrincipalOutstanding(currency);
if (principalOutstanding.isGreaterThanZero()) {
outstandingPrincipalBalance.set(outstandingPrincipalBalance.get().add(principalOutstanding));
i.addToPrincipal(loanTransaction.getTransactionDate(), principalOutstanding.negated());
if (loan.isInterestBearing()) {
if (((loanReAgeParameter.getInterestHandlingType() == null)
|| loanReAgeParameter.getInterestHandlingType().equals(LoanReAgeInterestHandlingType.DEFAULT))) {
// re-aging logic for interest-bearing loans
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
handleReAgeWithInterestRecalculationEnabled(loanTransaction, progressiveTransactionCtx);
} else if (loanTransaction.getLoan().isInterestBearing() && !loanTransaction.getLoan().isInterestRecalculationEnabled()) {
// TODO: implement interestRecalculation = false logic
throw new NotImplementedException(
"Logic for re-aging when interest bearing loan has interestRecalculation disabled is not implemented");
}
});
loanTransaction.updateComponentsAndTotal(outstandingPrincipalBalance.get(), Money.zero(currency), Money.zero(currency),
Money.zero(currency));
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 if (LoanReAgeInterestHandlingType.WAIVE_INTEREST.equals(loanReAgeParameter.getInterestHandlingType())) {
throw new NotImplementedException("WAIVE_INTEREST interest handling strategy for re-aging is not implemented");
} else {
LoanRepaymentScheduleInstallment lastNormalInstallment = lastNormalInstallmentOptional.get();
reAgedInstallmentNumber = lastNormalInstallment.getInstallmentNumber() + 1;
fromDate = lastNormalInstallment.getDueDate();
loan = lastNormalInstallment.getLoan();
}
LoanRepaymentScheduleInstallment reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(loan,
reAgedInstallmentNumber, fromDate, loanTransaction.getLoanReAgeParameter().getStartDate(),
calculatedPrincipal.getAmount());
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate());
for (int i = 1; i < loanTransaction.getLoanReAgeParameter().getNumberOfInstallments(); i++) {
LocalDate calculatedDueDate = calculateReAgedInstallmentDueDate(loanTransaction.getLoanReAgeParameter(),
reAgedInstallment.getDueDate());
int nextReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber() + 1;
reAgedInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(reAgedInstallment.getLoan(),
nextReAgedInstallmentNumber, reAgedInstallment.getDueDate(), calculatedDueDate, calculatedPrincipal.getAmount());
if (i + 1 == loanTransaction.getLoanReAgeParameter().getNumberOfInstallments()) {
reAgedInstallment.addToPrincipal(loanTransaction.getTransactionDate(), adjustCalculatedPrincipal);
if (LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.equals(loanReAgeParameter.getInterestHandlingType())) {
CommonReAgeSettings settings = new CommonReAgeSettings(false, true, true, true);
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestRecalculationEnabled()) {
handleReAgeEqualAmortizationEMICalculator(loanTransaction, settings, progressiveTransactionCtx);
} else {
handleReAgeWithCommonStrategy(loanTransaction, settings, ctx);
}
} else if (LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST
.equals(loanReAgeParameter.getInterestHandlingType())) {
CommonReAgeSettings settings = new CommonReAgeSettings(true, true, true, true);
if (ctx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loanTransaction.getLoan().isInterestRecalculationEnabled()) {
handleReAgeEqualAmortizationEMICalculator(loanTransaction, settings, progressiveTransactionCtx);
} else {
handleReAgeWithCommonStrategy(loanTransaction, settings, ctx);
}
}
insertOrReplaceRelatedInstallment(installments, reAgedInstallment, currency, loanTransaction.getTransactionDate());
}
int lastReAgedInstallmentNumber = reAgedInstallment.getInstallmentNumber();
List<LoanRepaymentScheduleInstallment> toRemove = installments.stream().filter(i -> i != null && !i.isAdditional()
&& i.getInstallmentNumber() != null && i.getInstallmentNumber() > lastReAgedInstallmentNumber).toList();
toRemove.forEach(installments::remove);
reprocessInstallments(installments);
} else {
handleReAgeWithCommonStrategy(loanTransaction, new CommonReAgeSettings(), ctx);
}
if (loanTransaction.getAmount().compareTo(ZERO) == 0) {
loanTransaction.reverse();
}
}
@@ -2929,7 +2941,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
final LoanRepaymentScheduleInstallment lastInstallment = ctx.getInstallments().getLast();
final LoanRepaymentScheduleInstallment newInstallment = LoanRepaymentScheduleInstallment.newReAgedInstallment(
loanTransaction.getLoan(), lastInstallment.getInstallmentNumber() + 1, rp.getFromDate(), rp.getDueDate(),
rp.getDuePrincipal().getAmount());
rp.getDuePrincipal().getAmount(), null, null, null);
if (rp.getDueInterest().isGreaterThanZero()) {
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) {
final MonetaryCurrency currency = ctx.getCurrency();
final Loan loan = loanTransaction.getLoan();
@@ -3250,6 +3550,105 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
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) {
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
@@ -24,11 +24,14 @@ import java.math.MathContext;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
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.ProgressiveLoanInterestScheduleModel;
@@ -151,4 +154,18 @@ public interface EMICalculator {
*/
@NotNull
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.MathUtil;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
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.domain.LoanRepaymentScheduleInstallment;
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.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
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;
@@ -1584,4 +1588,185 @@ public final class ProgressiveEMICalculator implements EMICalculator {
.filter(repaymentPeriod -> originalEmi.isLessThan(repaymentPeriod.getTotalPaidAmount())) //
.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() {
if (isPaused()) {
if (isPaused() || getRepaymentPeriod().isReAged()) {
return getCreditedInterest().getAmount();
}
@@ -92,9 +92,23 @@ public class RepaymentPeriod {
@Setter
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,
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.fromDate = fromDate;
this.dueDate = dueDate;
@@ -106,17 +120,22 @@ public class RepaymentPeriod {
this.futureUnrecognizedInterest = futureUnrecognizedInterest;
this.mc = mc;
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) {
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,
ILoanConfigurationDetails loanProductRelatedDetail) {
final Money zero = emi.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
newRepaymentPeriod.getInterestPeriods().add(InterestPeriod.withEmptyAmounts(newRepaymentPeriod, fromDate, dueDate));
return newRepaymentPeriod;
@@ -126,7 +145,8 @@ public class RepaymentPeriod {
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(),
repaymentPeriod.getDueDate(), new ArrayList<>(), repaymentPeriod.getEmi(), repaymentPeriod.getOriginalEmi(),
repaymentPeriod.getPaidPrincipal(), repaymentPeriod.getPaidInterest(), repaymentPeriod.getFutureUnrecognizedInterest(), mc,
repaymentPeriod.getLoanProductRelatedDetail());
repaymentPeriod.getLoanProductRelatedDetail(), repaymentPeriod.isNoUnrecognisedInterest(), repaymentPeriod.isReAged(),
repaymentPeriod.isReAgedEarlyRepaymentHolder(), repaymentPeriod.getReAgedInterest());
newRepaymentPeriod.setOutstandingMovedDueToReAging(repaymentPeriod.isOutstandingMovedDueToReAging());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
@@ -139,7 +159,8 @@ public class RepaymentPeriod {
final Money zero = Money.zero(repaymentPeriod.getCurrency(), mc);
final RepaymentPeriod newRepaymentPeriod = new RepaymentPeriod(previous, repaymentPeriod.getFromDate(),
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());
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
@@ -180,8 +201,8 @@ public class RepaymentPeriod {
@NotNull
public Money getCalculatedDueInterest() {
if (calculatedDueInterestCalculation == null) {
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { this.previous,
this.interestPeriods, this.futureUnrecognizedInterest, this.isInterestMoved, this.totalDisbursedAmount });
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest, () -> new Object[] { previous, interestPeriods,
futureUnrecognizedInterest, isInterestMoved, totalDisbursedAmount, reAgedInterest, reAged });
}
return calculatedDueInterestCalculation.get();
}
@@ -193,6 +214,7 @@ public class RepaymentPeriod {
getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(BigDecimal.ZERO, BigDecimal::add),
mc);
}
calculatedDueInterest = calculatedDueInterest.add(reAgedInterest);
calculatedDueInterest = calculatedDueInterest.add(getFutureUnrecognizedInterest(), getMc());
if (getPrevious().isPresent()) {
calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest(), getMc());
@@ -213,7 +235,7 @@ public class RepaymentPeriod {
: MathUtil.min(getCalculatedDueInterest(), getEmiPlusCreditedAmountsPlusFutureUnrecognizedInterest(), false),
getPaidInterest(), false),
() -> new Object[] { paidPrincipal, paidInterest, interestPeriods, futureUnrecognizedInterest, totalDisbursedAmount,
emi });
reAgedInterest, reAged, emi });
}
return dueInterestCalculation.get();
}
@@ -312,7 +334,7 @@ public class RepaymentPeriod {
* @return
*/
public Money getUnrecognizedInterest() {
return getCalculatedDueInterest().minus(getDueInterest(), getMc());
return noUnrecognisedInterest ? getZero() : getCalculatedDueInterest().minus(getDueInterest(), getMc());
}
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.TransactionCtx;
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.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
@@ -120,7 +121,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(InterestRefundService.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.setActionContext(ActionContext.DEFAULT);
@@ -490,7 +491,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
// Set up TransactionCtx with installments and charges
TransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, Set.of(), overpaymentHolder, changedTransactionDetail,
model);
model, null);
// Mock additional necessary methods
LoanCharge loanCharge = mock(LoanCharge.class);
@@ -18,7 +18,7 @@
*/
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.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.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.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.ScheduledDateGenerator;
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.OutstandingDetails;
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.RepaymentPeriod;
@@ -67,7 +73,7 @@ import org.springframework.lang.NonNull;
@ExtendWith(MockitoExtension.class)
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<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
private List<LoanScheduleModelRepaymentPeriod> generateExpectedRepaymentPeriods(LocalDate disbursementDate) {
return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {
@@ -122,7 +122,12 @@ class RepaymentPeriodTest {
null, // paidInterest
null, // futureUnrecognizedInterest
MC, // mc
loanProductRelatedDetail);
loanProductRelatedDetail, //
false, // noUnrecognizedInterest
false, // reAged
false, // reAgedEarlyRepaymentHolder
null // reAgedInterest
);
// Test that getters don't throw and return non-null
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.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
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,
List<LoanTransaction> collect) {
collect.add(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));
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);
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.getAmount().compareTo(refundFinal.get()) <= 0) {
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,
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);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
final List<LoanTransaction> newTransactions = reprocessResult.getLeft().getTransactionChanges().stream()
.map(TransactionChangeData::getNewTransaction).toList().stream().filter(LoanTransaction::isNotReversed).toList();
loan.getLoanTransactions().addAll(newTransactions);
@@ -196,7 +196,11 @@ public class LoanReAgingService {
}
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);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
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.PlatformApiDataValidationException;
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.LoanReAgingApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
@@ -113,6 +114,13 @@ public class LoanReAgingValidator {
}
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
LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loan.getLoanProductRelatedDetail().getLoanScheduleType().name());
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.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
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.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
@@ -137,8 +138,8 @@ public class LoanAccountAutoStarter {
final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService,
final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService,
@Lazy final LoanChargeService loanChargeService) {
@Lazy final LoanChargeService loanChargeService, final ScheduledDateGenerator scheduledDateGenerator) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, progressiveLoanInterestRefundService, externalIdFactory,
loanSchedule, loanChargeValidator, loanBalanceService, loanChargeService);
loanSchedule, loanChargeValidator, loanBalanceService, loanChargeService, scheduledDateGenerator);
}
}