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