FINERACT-1981: EMI calculator rework

This commit is contained in:
Janos Meszaros
2024-10-16 14:35:58 +02:00
committed by Adam Saghy
parent d4190ede90
commit bb072709b5
17 changed files with 1082 additions and 1146 deletions
@@ -481,4 +481,14 @@ public final class MathUtil {
public static BigDecimal stripTrailingZeros(final BigDecimal value) {
return value == null ? null : new BigDecimal(value.stripTrailingZeros().toPlainString());
}
/**
* @return calculates maximum of the two values considering null values
* @param notNull
* if true then null parameter is omitted, otherwise returns null
*/
public static Money max(Money first, Money second, boolean notNull) {
return notNull ? first == null ? second : second == null ? first : max(first, second, false)
: isGreaterThan(first, second) ? first : second;
}
}
@@ -497,10 +497,7 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
.recalculationRestFrequencyType(2)//
.recalculationRestFrequencyInterval(1)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT")));//
Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvancedpaymentInterest36030InterestRecalcTillPreClose = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcTillPreclose).execute();
TestContext.INSTANCE.set(
@@ -5539,7 +5539,6 @@ Feature: Loan
Scenario: Early pay-off loan with interest, TILL_PRECLOSE product
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
@@ -5595,7 +5594,6 @@ Feature: Loan
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false |
| 15 February 2024 | Repayment | 83.81 | 83.57 | 0.24 | 0.0 | 0.0 | 0.0 | false |
Then Loan's all installments have obligations met
When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "LAST_INSTALLMENT" future installment allocation rule
Scenario: Early pay-off loan with interest, TILL_REST_FREQUENCY_DATE product
When Admin sets the business date to "01 January 2024"
@@ -5756,4 +5754,4 @@ Feature: Loan
Then Loan term variations has 2 variation, with the following data:
| Term Type Id | Term Type Code | Term Type Value | Applicable From | Decimal Value | Date Value | Is Specific To Installment | Is Processed |
| 1 | loanTermType.emiAmount | emiAmount | 01 January 2023 | 50.0 | | false | |
| 4 | loanTermType.dueDate | dueDate | 01 February 2023 | 50.0 | 15 January 2023 | false | |
| 4 | loanTermType.dueDate | dueDate | 01 February 2023 | 50.0 | 15 January 2023 | false | |
@@ -79,7 +79,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParamet
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -171,12 +171,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency));
final MathContext mc = MoneyHelper.getMathContext();
final Loan loan = loanTransactions.get(0).getLoan();
final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
installmentAmountInMultiplesOf, installments, mc);
installmentAmountInMultiplesOf, installments);
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
changedTransactionDetail, scheduleModel);
@@ -204,7 +203,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId);
createNewTransaction(oldTransaction, newTransaction, ctx);
}
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx, true);
recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx);
List<LoanTransaction> txs = chargeOrTransactions.stream() //
.filter(ChargeOrTransaction::isTransaction) //
.map(e -> e.getLoanTransaction().get()).toList();
@@ -778,14 +777,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
amortizableAmount);
if (amortizableAmount.isGreaterThanZero()) {
progressiveTransactionCtx.getModel().repayments().forEach(rm -> {
progressiveTransactionCtx.getModel().repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = transactionCtx.getInstallments().stream()
.filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()
&& !ri.getDueDate().isBefore(disbursementTransaction.getTransactionDate()))
.findFirst().orElse(null);
if (installment != null) {
installment.updatePrincipal(rm.getPrincipalDue().getAmount());
installment.updateInterestCharged(rm.getInterestDue().getAmount());
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
installment.updateInterestCharged(rm.getDueInterest().getAmount());
installment.updateObligationsMet(progressiveTransactionCtx.getCurrency(), disbursementTransaction.getTransactionDate());
}
});
@@ -877,7 +876,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList();
}
private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransactionCtx ctx, boolean isLastRecalculation) {
private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransactionCtx ctx) {
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()
&& ctx.getInstallments().get(0).getLoan().isInterestRecalculationEnabledForProduct()
&& !ctx.getInstallments().get(0).getLoan().isNpa() && !ctx.getInstallments().get(0).getLoan().isChargedOff()) {
@@ -897,27 +896,32 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanRepaymentScheduleInstallment currentInstallment = currentInstallmentOptional.orElse(lastInstallment);
Money overDuePrincipal = Money.zero(ctx.getCurrency());
Money aggregatedOverDuePrincipal = Money.zero(ctx.getCurrency());
for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) {
// add and subtract outstanding principal
if (!overDuePrincipal.isZero()) {
adjustOverduePrincipalForInstallment(currentDate, isLastRecalculation, processingInstallment, overDuePrincipal,
ctx);
adjustOverduePrincipalForInstallment(currentDate, processingInstallment, overDuePrincipal,
aggregatedOverDuePrincipal, ctx);
}
overDuePrincipal = overDuePrincipal.add(processingInstallment.getPrincipalOutstanding(ctx.getCurrency()).getAmount());
overDuePrincipal = processingInstallment.getPrincipalOutstanding(ctx.getCurrency());
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
}
boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(currentDate);
if (adjustNeeded) {
adjustOverduePrincipalForInstallment(currentDate, isLastRecalculation, currentInstallment, overDuePrincipal, ctx);
adjustOverduePrincipalForInstallment(currentDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal,
ctx);
}
}
}
}
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, boolean isLastRecalculation,
LoanRepaymentScheduleInstallment currentInstallment, Money overduePrincipal, ProgressiveTransactionCtx ctx) {
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, LoanRepaymentScheduleInstallment currentInstallment,
Money overduePrincipal, Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
LocalDate fromDate = currentInstallment.getFromDate();
LocalDate toDate = currentInstallment.getDueDate();
boolean hasUpdate = false;
if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment()) {
@@ -927,6 +931,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
emiCalculator.addBalanceCorrection(ctx.getModel(), fromDate, overduePrincipal);
ctx.setLastOverdueBalanceChange(fromDate);
hasUpdate = true;
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
emiCalculator.addBalanceCorrection(ctx.getModel(), currentInstallment.getDueDate(),
aggregatedOverDuePrincipal.negated());
ctx.setLastOverdueBalanceChange(currentInstallment.getDueDate());
}
}
}
@@ -946,8 +956,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// setting negative correction for the period from current date, expecting the overdue balance's full
// repayment on that day.
if (currentDate.isAfter(currentInstallment.getFromDate()) && currentDate.isBefore(currentInstallment.getDueDate())) {
emiCalculator.addBalanceCorrection(ctx.getModel(), currentDate, overduePrincipal.negated());
// TODO: we might need to do it outside of this method only for the current date at the end
if (currentDate.isAfter(currentInstallment.getFromDate()) && !currentDate.isAfter(currentInstallment.getDueDate())) {
emiCalculator.addBalanceCorrection(ctx.getModel(), currentDate, aggregatedOverDuePrincipal.negated());
ctx.setLastOverdueBalanceChange(currentDate);
}
hasUpdate = true;
@@ -959,13 +970,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) {
ctx.getModel().repayments().forEach(repayment -> {
ctx.getModel().repaymentPeriods().forEach(repayment -> {
LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream()
.filter(ri -> !ri.isDownPayment() && Objects.equals(ri.getFromDate(), repayment.getFromDate())) //
.findFirst().orElse(null);
if (installment != null) {
installment.updatePrincipal(repayment.getPrincipalDue().getAmount());
installment.updateInterestCharged(repayment.getInterestDue().getAmount());
installment.updatePrincipal(repayment.getDuePrincipal().getAmount());
installment.updateInterestCharged(repayment.getDueInterest().getAmount());
installment.setRecalculatedInterestComponent(true);
}
});
@@ -973,7 +984,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private void handleRepayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
if (transactionCtx instanceof ProgressiveTransactionCtx) {
recalculateInterestForDate(loanTransaction.getTransactionDate(), (ProgressiveTransactionCtx) transactionCtx, false);
recalculateInterestForDate(loanTransaction.getTransactionDate(), (ProgressiveTransactionCtx) transactionCtx);
}
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents();
@@ -1412,9 +1423,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, oldestPastDueInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
Loan loan = loanTransaction.getLoan();
if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing()
&& loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction,
transactionAmountUnprocessed, balances, paymentAllocationType, oldestPastDueInstallment, ctx,
loanTransactionToRepaymentScheduleMapping, oldestPastDueInstallmentCharges);
} else {
paidPortion = processPaymentAllocation(paymentAllocationType, oldestPastDueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
oldestPastDueInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
}
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} else {
exit = true;
@@ -1426,9 +1445,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
firstNormalInstallmentNumber);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, dueInstallment, currency);
paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
Loan loan = loanTransaction.getLoan();
if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing()
&& loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction,
transactionAmountUnprocessed, balances, paymentAllocationType, dueInstallment, ctx,
loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges);
} else {
paidPortion = processPaymentAllocation(paymentAllocationType, dueInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, dueInstallmentCharges,
balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
}
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
} else {
exit = true;
@@ -1452,56 +1479,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
Loan loan = loanTransaction.getLoan();
if (transactionCtx instanceof ProgressiveTransactionCtx ctx && loan.isInterestBearing()
&& loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
LoanPreClosureInterestCalculationStrategy strategy = loanTransaction.getLoan().getLoanProduct()
.preCloseInterestCalculationStrategy();
LocalDate payDate = switch (strategy) {
case TILL_PRE_CLOSURE_DATE -> {
LocalDate transactionDate = loanTransaction.getTransactionDate();
yield inAdvanceInstallment.getFromDate().isAfter(transactionDate)
? inAdvanceInstallment.getFromDate()
: transactionDate;
}
case TILL_REST_FREQUENCY_DATE -> inAdvanceInstallment.getDueDate();
case NONE ->
throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
};
ProgressiveLoanInterestRepaymentModel payableDetails = emiCalculator
.getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate).orElseThrow();
switch (paymentAllocationType) {
case IN_ADVANCE_INTEREST ->
inAdvanceInstallment.updateInterestCharged(payableDetails.getInterestDue().getAmount());
case IN_ADVANCE_PRINCIPAL ->
inAdvanceInstallment.updatePrincipal(payableDetails.getPrincipalDue().getAmount());
default -> {
}
}
paidPortion = processPaymentAllocation(paymentAllocationType, inAdvanceInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping,
inAdvanceInstallmentCharges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
switch (paymentAllocationType) {
case IN_ADVANCE_PRINCIPAL -> {
Money balance = switch (strategy) {
case TILL_PRE_CLOSURE_DATE -> payableDetails.getOutstandingBalance();
case TILL_REST_FREQUENCY_DATE -> payableDetails.getRemainingBalance();
default -> throw new IllegalStateException();
};
emiCalculator.addBalanceCorrection(model, payDate, balance.multipliedBy(-1));
emiCalculator.addBalanceCorrection(model, payDate,
payableDetails.getPrincipalDue().minus(paidPortion));
}
case IN_ADVANCE_INTEREST -> {
emiCalculator.addBalanceCorrection(model, payDate,
payableDetails.getInterestDue().minus(paidPortion));
}
default -> {
}
}
paidPortion = handlingPaymentAllocationForInterestBearingProgressiveLoan(loanTransaction, evenPortion,
balances, paymentAllocationType, inAdvanceInstallment, ctx,
loanTransactionToRepaymentScheduleMapping, inAdvanceInstallmentCharges);
} else {
// Adjust the portion for the last installment
if (inAdvanceInstallment.equals(inAdvanceInstallments.get(numberOfInstallments - 1))) {
@@ -1527,8 +1507,74 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
return transactionAmountUnprocessed;
}
private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTransaction loanTransaction,
Money transactionAmountUnprocessed, Balances balances, PaymentAllocationType paymentAllocationType,
LoanRepaymentScheduleInstallment installment, ProgressiveTransactionCtx ctx,
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set<LoanCharge> charges) {
Money paidPortion;
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
LocalDate payDate = loanTransaction.getTransactionDate();
if (DueType.IN_ADVANCE.equals(paymentAllocationType.getDueType())) {
payDate = calculateNewPayDateInCaseOfInAdvancePayment(loanTransaction, installment);
updateRepaymentPeriodBalances(paymentAllocationType, installment, model, payDate);
}
paidPortion = processPaymentAllocation(paymentAllocationType, installment, loanTransaction, transactionAmountUnprocessed,
loanTransactionToRepaymentScheduleMapping, charges, balances, LoanRepaymentScheduleInstallment.PaymentAction.PAY);
if (PRINCIPAL.equals(paymentAllocationType.getAllocationType())) {
emiCalculator.payPrincipal(model, installment.getDueDate(), payDate, paidPortion);
updateRepaymentPeriods(loanTransaction, ctx, model);
} else if (INTEREST.equals(paymentAllocationType.getAllocationType())) {
emiCalculator.payInterest(model, installment.getDueDate(), payDate, paidPortion);
updateRepaymentPeriods(loanTransaction, ctx, model);
}
return paidPortion;
}
private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx,
ProgressiveLoanInterestScheduleModel model) {
model.repaymentPeriods().forEach(rm -> {
LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream()
.filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()).findFirst().orElse(null);
if (installment != null) {
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
installment.updateInterestCharged(rm.getDueInterest().getAmount());
installment.updateObligationsMet(ctx.getCurrency(), loanTransaction.getTransactionDate());
}
});
}
private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType,
LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) {
PayableDetails payableDetails = emiCalculator.getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate);
switch (paymentAllocationType) {
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getPayableInterest().getAmount());
case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getPayablePrincipal().getAmount());
default -> {
}
}
}
private LocalDate calculateNewPayDateInCaseOfInAdvancePayment(LoanTransaction loanTransaction,
LoanRepaymentScheduleInstallment inAdvanceInstallment) {
LoanPreClosureInterestCalculationStrategy strategy = loanTransaction.getLoan().getLoanProduct()
.preCloseInterestCalculationStrategy();
LocalDate payDate = switch (strategy) {
case TILL_PRE_CLOSURE_DATE -> loanTransaction.getTransactionDate();
case TILL_REST_FREQUENCY_DATE -> loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getFromDate()) //
&& !loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getDueDate()) //
? inAdvanceInstallment.getDueDate() //
: loanTransaction.getTransactionDate(); //
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
};
return payDate;
}
@NotNull
private static Set<LoanCharge> getLoanChargesOfInstallment(Set<LoanCharge> charges, LoanRepaymentScheduleInstallment currentInstallment,
private Set<LoanCharge> getLoanChargesOfInstallment(Set<LoanCharge> charges, LoanRepaymentScheduleInstallment currentInstallment,
int firstNormalInstallmentNumber) {
return charges.stream().filter(loanCharge -> currentInstallment.getInstallmentNumber().equals(firstNormalInstallmentNumber)
? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(currentInstallment.getFromDate(),
@@ -0,0 +1,106 @@
/**
* 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.loanaccount.loanschedule.data;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
@Getter
@ToString(exclude = { "repaymentPeriod" })
@EqualsAndHashCode(exclude = { "repaymentPeriod" })
public class InterestPeriod implements Comparable<InterestPeriod> {
private final RepaymentPeriod repaymentPeriod;
private final LocalDate fromDate;
@Setter
@NotNull
private LocalDate dueDate;
@Setter
private BigDecimal rateFactor;
private Money disbursementAmount;
private Money balanceCorrectionAmount;
private Money outstandingLoanBalance;
public InterestPeriod(RepaymentPeriod repaymentPeriod, LocalDate fromDate, LocalDate dueDate, BigDecimal rateFactor,
Money disbursementAmount, Money balanceCorrectionAmount, Money outstandingLoanBalance) {
this.repaymentPeriod = repaymentPeriod;
this.fromDate = fromDate;
this.dueDate = dueDate;
this.rateFactor = rateFactor;
this.disbursementAmount = disbursementAmount;
this.balanceCorrectionAmount = balanceCorrectionAmount;
this.outstandingLoanBalance = outstandingLoanBalance;
}
public InterestPeriod(RepaymentPeriod repaymentPeriod, InterestPeriod interestPeriod) {
this(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), interestPeriod.getRateFactor(),
interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(),
interestPeriod.getOutstandingLoanBalance());
}
@Override
public int compareTo(@NotNull InterestPeriod o) {
return dueDate.compareTo(o.dueDate);
}
public void addBalanceCorrectionAmount(final Money balanceCorrectionAmount) {
this.balanceCorrectionAmount = MathUtil.plus(this.balanceCorrectionAmount, balanceCorrectionAmount);
}
public void addDisbursementAmount(final Money disbursementAmount) {
this.disbursementAmount = MathUtil.plus(this.disbursementAmount, disbursementAmount);
}
public Money getCalculatedDueInterest() {
return getOutstandingLoanBalance().multipliedBy(getRateFactor());
}
public void updateOutstandingLoanBalance() {
if (isFirstInterestPeriod()) {
Optional<RepaymentPeriod> previousRepaymentPeriod = getRepaymentPeriod().getPrevious();
if (previousRepaymentPeriod.isPresent()) {
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getInterestPeriods()
.get(previousRepaymentPeriod.get().getInterestPeriods().size() - 1);
this.outstandingLoanBalance = previousInterestPeriod.getOutstandingLoanBalance()//
.plus(previousInterestPeriod.getDisbursementAmount())//
.plus(previousInterestPeriod.getBalanceCorrectionAmount())//
.minus(previousRepaymentPeriod.get().getDuePrincipal())//
.plus(previousRepaymentPeriod.get().getPaidPrincipal());//
}
} else {
int index = getRepaymentPeriod().getInterestPeriods().indexOf(this);
InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1);
this.outstandingLoanBalance = previousInterestPeriod.getOutstandingLoanBalance() //
.plus(previousInterestPeriod.getBalanceCorrectionAmount()) //
.plus(previousInterestPeriod.getDisbursementAmount()); //
}
}
private boolean isFirstInterestPeriod() {
return getRepaymentPeriod().getInterestPeriods().get(0).equals(this);
}
}
@@ -22,11 +22,13 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import org.jetbrains.annotations.NotNull;
public record ProgressiveLoanInterestRate(LocalDate effectiveFrom, LocalDate validFrom,
BigDecimal interestRate) implements Comparable<ProgressiveLoanInterestRate> {
public record InterestRate(LocalDate effectiveFrom, //
LocalDate validFrom, //
BigDecimal interestRate //
) implements Comparable<InterestRate> {
@Override
public int compareTo(@NotNull ProgressiveLoanInterestRate o) {
public int compareTo(@NotNull InterestRate o) {
return this.effectiveFrom().compareTo(o.effectiveFrom());
}
}
@@ -16,29 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanproduct.calc;
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
import java.math.BigDecimal;
import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Data;
import org.apache.fineract.organisation.monetary.domain.Money;
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
public class EMICalculationResult {
@Data
public class PayableDetails {
@Getter
private final Money equalMonthlyInstallmentValue;
private final List<BigDecimal> repaymentPeriodRateFactorMinus1List;
private int counter = 0;
public BigDecimal getNextRepaymentPeriodRateFactorMinus1() {
return counter < repaymentPeriodRateFactorMinus1List.size() ? repaymentPeriodRateFactorMinus1List.get(counter++) : BigDecimal.ZERO;
}
public void reset() {
counter = 0;
}
private final Money emi;
private final Money payablePrincipal;
private final Money payableInterest;
private final Money outstandingBalance;
}
@@ -1,61 +0,0 @@
/**
* 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.loanaccount.loanschedule.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.jetbrains.annotations.NotNull;
@AllArgsConstructor
@Data
public class ProgressiveLoanInterestRepaymentInterestPeriod implements Comparable<ProgressiveLoanInterestRepaymentInterestPeriod> {
private LocalDate fromDate;
private LocalDate dueDate;
private BigDecimal rateFactorMinus1;
private Money disbursedAmount;
private Money correctionAmount;
private Money interestDue;
public ProgressiveLoanInterestRepaymentInterestPeriod(final ProgressiveLoanInterestRepaymentInterestPeriod period) {
this(period.fromDate, period.dueDate, period.rateFactorMinus1, period.disbursedAmount, period.correctionAmount, period.interestDue);
}
@Override
public int compareTo(@NotNull ProgressiveLoanInterestRepaymentInterestPeriod o) {
return dueDate.compareTo(o.dueDate);
}
public void addDisbursedAmount(final Money outstandingBalance) {
if (outstandingBalance != null && !outstandingBalance.isZero()) {
this.disbursedAmount = this.disbursedAmount.add(outstandingBalance);
}
}
public void addCorrectionAmount(final Money correctionAmount) {
if (correctionAmount != null && !correctionAmount.isZero()) {
this.correctionAmount = this.correctionAmount.add(correctionAmount);
}
}
}
@@ -1,99 +0,0 @@
/**
* 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.loanaccount.loanschedule.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.LinkedList;
import lombok.Data;
import org.apache.fineract.organisation.monetary.domain.Money;
@Data
public class ProgressiveLoanInterestRepaymentModel {
private final LocalDate fromDate;
private final LocalDate dueDate;
private LinkedList<ProgressiveLoanInterestRepaymentInterestPeriod> interestPeriods;
private boolean isLastPeriod;
private Money equalMonthlyInstallment;
private Money principalDue;
private Money remainingBalance;
private Money initialBalance;
public ProgressiveLoanInterestRepaymentModel(final LocalDate fromDate, final LocalDate dueDate, final Money equalMonthlyInstallment) {
this.fromDate = fromDate;
this.dueDate = dueDate;
this.equalMonthlyInstallment = equalMonthlyInstallment;
this.isLastPeriod = false;
final Money zeroAmount = Money.zero(equalMonthlyInstallment.getCurrency());
this.initialBalance = zeroAmount;
this.remainingBalance = zeroAmount;
this.principalDue = zeroAmount;
this.interestPeriods = new LinkedList<>();
this.interestPeriods.add(
new ProgressiveLoanInterestRepaymentInterestPeriod(fromDate, dueDate, BigDecimal.ZERO, zeroAmount, zeroAmount, zeroAmount));
}
public ProgressiveLoanInterestRepaymentModel(ProgressiveLoanInterestRepaymentModel repaymentModel) {
this.fromDate = repaymentModel.fromDate;
this.dueDate = repaymentModel.dueDate;
this.isLastPeriod = repaymentModel.isLastPeriod;
this.equalMonthlyInstallment = repaymentModel.equalMonthlyInstallment;
this.initialBalance = repaymentModel.initialBalance;
this.remainingBalance = repaymentModel.remainingBalance;
this.principalDue = repaymentModel.principalDue;
this.interestPeriods = new LinkedList<>();
for (final ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod : repaymentModel.interestPeriods) {
this.interestPeriods.add(new ProgressiveLoanInterestRepaymentInterestPeriod(interestPeriod));
}
}
public BigDecimal getRateFactor() {
return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getRateFactorMinus1).reduce(BigDecimal.ONE,
BigDecimal::add);
}
public Money getDisbursedAmountInPeriod() {
return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getDisbursedAmount)
.reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus);
}
public Money getInterestDue() {
return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getInterestDue)
.reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus);
}
public Money getCorrectionAmount() {
return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getCorrectionAmount)
.reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus);
}
public Money getOutstandingBalance() {
return initialBalance.plus(getDisbursedAmountInPeriod());
}
public Money getCorrectedOutstandingBalance() {
return getOutstandingBalance().plus(getCorrectionAmount());
}
}
@@ -19,28 +19,56 @@
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import lombok.Data;
import lombok.experimental.Accessors;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
public record ProgressiveLoanInterestScheduleModel(List<ProgressiveLoanInterestRepaymentModel> repayments, //
List<ProgressiveLoanInterestRate> interestRates, //
LoanProductRelatedDetail loanProductRelatedDetail, //
Integer installmentAmountInMultiplesOf, //
MathContext mc) {
@Data
@Accessors(fluent = true)
public class ProgressiveLoanInterestScheduleModel {
public ProgressiveLoanInterestScheduleModel(List<ProgressiveLoanInterestRepaymentModel> repayments,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) {
this(repayments, new ArrayList<>(1), loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
private final List<RepaymentPeriod> repaymentPeriods;
private final List<InterestRate> interestRates;
private final LoanProductRelatedDetail loanProductRelatedDetail;
private final Integer installmentAmountInMultiplesOf;
public ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf) {
this.repaymentPeriods = repaymentPeriods;
this.interestRates = new ArrayList<>();
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
}
public void addInterestRate(final LocalDate newInterestDueDate, final BigDecimal newInterestRate) {
interestRates.add(new ProgressiveLoanInterestRate(newInterestDueDate, newInterestDueDate.plusDays(1), newInterestRate));
interestRates.sort(Collections.reverseOrder());
private ProgressiveLoanInterestScheduleModel(List<RepaymentPeriod> repaymentPeriods, final List<InterestRate> interestRates,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf) {
this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods);
this.interestRates = new ArrayList<>(interestRates);
this.loanProductRelatedDetail = loanProductRelatedDetail;
this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf;
}
public ProgressiveLoanInterestScheduleModel deepCopy() {
return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, interestRates, loanProductRelatedDetail,
installmentAmountInMultiplesOf);
}
private List<RepaymentPeriod> copyRepaymentPeriods(final List<RepaymentPeriod> repaymentPeriods) {
final List<RepaymentPeriod> repaymentCopies = new ArrayList<>(repaymentPeriods.size());
RepaymentPeriod previousPeriod = null;
for (RepaymentPeriod repaymentPeriod : repaymentPeriods) {
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentPeriod);
previousPeriod = currentPeriod;
repaymentCopies.add(currentPeriod);
}
return repaymentCopies;
}
public BigDecimal getInterestRate(final LocalDate effectiveDate) {
@@ -48,16 +76,110 @@ public record ProgressiveLoanInterestScheduleModel(List<ProgressiveLoanInterestR
}
private BigDecimal findInterestRate(final LocalDate effectiveDate) {
return interestRates.stream().filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate))
.map(ProgressiveLoanInterestRate::interestRate).findFirst().orElse(loanProductRelatedDetail.getAnnualNominalInterestRate());
return interestRates.stream().filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)).map(InterestRate::interestRate).findFirst()
.orElse(loanProductRelatedDetail.getAnnualNominalInterestRate());
}
public Optional<RepaymentPeriod> findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) {
if (repaymentPeriodDueDate == null) {
return Optional.empty();
}
return repaymentPeriods.stream()//
.filter(repaymentPeriodItem -> repaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))//
.findFirst();
}
public List<RepaymentPeriod> getRelatedRepaymentPeriods(final LocalDate calculateFromRepaymentPeriodDueDate) {
if (calculateFromRepaymentPeriodDueDate == null) {
return repaymentPeriods;
}
return repaymentPeriods.stream()//
.filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))//
.toList();//
}
public int getLoanTermInDays() {
if (repayments.isEmpty()) {
if (repaymentPeriods.isEmpty()) {
return 0;
}
final var firstPeriod = repayments.get(0);
final var lastPeriod = repayments.size() > 1 ? repayments.get(repayments.size() - 1) : firstPeriod;
final RepaymentPeriod firstPeriod = repaymentPeriods.get(0);
final RepaymentPeriod lastPeriod = repaymentPeriods.size() > 1 ? repaymentPeriods.get(repaymentPeriods.size() - 1) : firstPeriod;
return Math.toIntExact(ChronoUnit.DAYS.between(firstPeriod.getFromDate(), lastPeriod.getDueDate()));
}
public LocalDate getMaturityDate() {
return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(repaymentPeriods.size() - 1).getDueDate() : null;
}
public Optional<RepaymentPeriod> changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate,
final Money disbursedAmount, final Money correctionAmount) {
return findRepaymentPeriodForBalanceChange(balanceChangeDate).stream()//
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount))//
.findFirst();//
}
Optional<RepaymentPeriod> findRepaymentPeriodForBalanceChange(final LocalDate balanceChangeDate) {
if (balanceChangeDate == null) {
return Optional.empty();
}
return repaymentPeriods.stream()//
.filter(repaymentPeriod -> {
if (repaymentPeriod.getPrevious().isPresent()) {
return balanceChangeDate.isAfter(repaymentPeriod.getFromDate())
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
} else {
return !balanceChangeDate.isBefore(repaymentPeriod.getFromDate())
&& !balanceChangeDate.isAfter(repaymentPeriod.getDueDate());
}
})//
.findFirst();
}
private Consumer<RepaymentPeriod> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount,
final Money correctionAmount) {
return repaymentPeriod -> {
final Optional<InterestPeriod> interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate);
if (interestPeriodOptional.isPresent()) {
interestPeriodOptional.get().addDisbursementAmount(disbursedAmount);
interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount);
} else {
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount);
}
};
}
Optional<InterestPeriod> findInterestPeriodForBalanceChange(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate) {
if (repaymentPeriod == null || balanceChangeDate == null) {
return Optional.empty();
}
return repaymentPeriod.getInterestPeriods().stream()//
.filter(interestPeriod -> balanceChangeDate.isEqual(interestPeriod.getDueDate()))//
.findFirst();
}
void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate, final Money disbursedAmount,
final Money correctionAmount) {
final InterestPeriod previousInterestPeriod;
if (balanceChangeDate.isAfter(repaymentPeriod.getFromDate())) {
previousInterestPeriod = repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1);//
} else {
previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream()
.filter(ip -> balanceChangeDate.isAfter(ip.getFromDate()) && !balanceChangeDate.isAfter(ip.getDueDate()))//
.reduce((first, second) -> second)//
.orElse(repaymentPeriod.getInterestPeriods().get(0));
}
LocalDate originalDueDate = previousInterestPeriod.getDueDate();
LocalDate newDueDate = balanceChangeDate.isBefore(previousInterestPeriod.getFromDate()) ? previousInterestPeriod.getFromDate()
: balanceChangeDate.isAfter(previousInterestPeriod.getDueDate()) ? previousInterestPeriod.getDueDate() : balanceChangeDate;
previousInterestPeriod.setDueDate(newDueDate);
previousInterestPeriod.addDisbursementAmount(disbursedAmount);
previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount);
final InterestPeriod interestPeriod = new InterestPeriod(repaymentPeriod, previousInterestPeriod.getDueDate(), originalDueDate,
BigDecimal.ZERO, getZero(), getZero(), getZero());
repaymentPeriod.getInterestPeriods().add(interestPeriod);
}
private Money getZero() {
return Money.zero(loanProductRelatedDetail.getCurrency());
}
}
@@ -0,0 +1,152 @@
/**
* 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.loanaccount.loanschedule.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
@ToString(exclude = { "previous" })
@EqualsAndHashCode(exclude = { "previous" })
public class RepaymentPeriod {
private final RepaymentPeriod previous;
@Getter
private final LocalDate fromDate;
@Getter
private final LocalDate dueDate;
@Getter
private final List<InterestPeriod> interestPeriods;
@Setter
@Getter
private Money emi;
@Getter
private Money paidPrincipal;
@Getter
private Money paidInterest;
public RepaymentPeriod(RepaymentPeriod previous, LocalDate fromDate, LocalDate dueDate, Money emi) {
this.previous = previous;
this.fromDate = fromDate;
this.dueDate = dueDate;
this.emi = emi;
this.interestPeriods = new ArrayList<>();
// There is always at least 1 interest period, by default with same from-due date as repayment period
getInterestPeriods().add(new InterestPeriod(this, getFromDate(), getDueDate(), BigDecimal.ZERO, getZero(), getZero(), getZero()));
this.paidInterest = getZero();
this.paidPrincipal = getZero();
}
public RepaymentPeriod(RepaymentPeriod previous, RepaymentPeriod repaymentPeriod) {
this.previous = previous;
this.fromDate = repaymentPeriod.fromDate;
this.dueDate = repaymentPeriod.dueDate;
this.emi = repaymentPeriod.emi;
this.interestPeriods = new ArrayList<>();
this.paidPrincipal = repaymentPeriod.paidPrincipal;
this.paidInterest = repaymentPeriod.paidInterest;
// There is always at least 1 interest period, by default with same from-due date as repayment period
for (InterestPeriod interestPeriod : repaymentPeriod.interestPeriods) {
interestPeriods.add(new InterestPeriod(this, interestPeriod));
}
}
public Optional<RepaymentPeriod> getPrevious() {
return Optional.ofNullable(previous);
}
public BigDecimal getRateFactorPlus1() {
return interestPeriods.stream().map(InterestPeriod::getRateFactor).reduce(BigDecimal.ONE, BigDecimal::add);
}
public Money getCalculatedDueInterest() {
Money calculatedDueInterest = getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest).reduce(getZero(),
Money::plus);
if (getPrevious().isPresent()) {
calculatedDueInterest = calculatedDueInterest.add(getPrevious().get().getUnrecognizedInterest());
}
return calculatedDueInterest;
}
private Money getZero() {
// EMI is always initiated
return this.emi.zero();
}
public Money getCalculatedDuePrincipal() {
return getEmi().minus(getCalculatedDueInterest());
}
public boolean isFullyPaid() {
return getEmi().isEqualTo(getPaidPrincipal().plus(getPaidInterest()));
}
public Money getDueInterest() {
// Due interest might be the maximum paid if there is pay-off or early repayment
return MathUtil.max(getPaidPrincipal().isGreaterThan(getCalculatedDuePrincipal()) ? getPaidInterest() : getCalculatedDueInterest(),
getPaidInterest(), false);
}
public Money getDuePrincipal() {
// Due principal might be the maximum paid if there is pay-off or early repayment
return MathUtil.max(getEmi().minus(getDueInterest()), getPaidPrincipal(), false);
}
public Money getUnrecognizedInterest() {
return getCalculatedDueInterest().minus(getDueInterest());
}
public Money getOutstandingLoanBalance() {
InterestPeriod lastInstallmentPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1);
Money calculatedOutStandingLoanBalance = lastInstallmentPeriod.getOutstandingLoanBalance() //
.plus(lastInstallmentPeriod.getBalanceCorrectionAmount()) //
.plus(lastInstallmentPeriod.getDisbursementAmount()) //
.minus(getDuePrincipal())//
.plus(getPaidPrincipal());//
return MathUtil.negativeToZero(calculatedOutStandingLoanBalance);
}
public void addPaidPrincipalAmount(Money paidPrincipal) {
this.paidPrincipal = MathUtil.plus(this.paidPrincipal, paidPrincipal);
}
public void addPaidInterestAmount(Money paidInterest) {
this.paidInterest = MathUtil.plus(this.paidInterest, paidInterest);
}
public Money getInitialBalanceForEmiRecalculation() {
Money initialBalance;
if (getPrevious().isPresent()) {
initialBalance = getPrevious().get().getOutstandingLoanBalance();
} else {
initialBalance = getZero();
}
Money totalDisbursedAmount = getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount).reduce(getZero(),
Money::plus);
return initialBalance.add(totalDisbursedAmount);
}
}
@@ -18,8 +18,6 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
@@ -47,7 +45,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -78,7 +76,6 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
}
// determine the total charges due at time of disbursement
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
// setup variables for tracking important facts required for loan
@@ -100,8 +97,8 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
.generateRepaymentPeriods(periodStartDate, loanApplicationTerms, holidayDetailDTO);
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel(
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetail(),
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size() + 2);
loanApplicationTerms.getInstallmentAmountInMultiplesOf());
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms);
@@ -113,34 +110,33 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate());
processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods,
chargesDueAtTimeOfDisbursement);
chargesDueAtTimeOfDisbursement, false);
repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber());
if (loanApplicationTerms.getLoanTermVariations() != null) {
for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) {
final LocalDate interestRateChangeEffectiveDate = interestRateChange.getTermVariationApplicableFrom().minusDays(1);
final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
final BigDecimal newInterestRate = interestRateChange.getDecimalValue();
if (interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getFromDate())
&& !interestRateChangeEffectiveDate.isAfter(repaymentPeriod.getDueDate())) {
emiCalculator.changeInterestRate(interestScheduleModel, interestRateChangeEffectiveDate, newInterestRate);
if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate())
&& !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) {
emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate);
}
}
}
emiCalculator.findInterestRepaymentPeriod(interestScheduleModel, repaymentPeriod.getDueDate())
.ifPresent(interestRepaymentPeriod -> {
final Money principalDue = interestRepaymentPeriod.getPrincipalDue();
final Money interestDue = interestRepaymentPeriod.getInterestDue();
emiCalculator.findRepaymentPeriod(interestScheduleModel, repaymentPeriod.getDueDate()).ifPresent(interestRepaymentPeriod -> {
final Money principalDue = interestRepaymentPeriod.getDuePrincipal();
final Money interestDue = interestRepaymentPeriod.getDueInterest();
repaymentPeriod.addPrincipalAmount(principalDue);
repaymentPeriod.addInterestAmount(interestDue);
repaymentPeriod.setOutstandingLoanBalance(interestRepaymentPeriod.getRemainingBalance());
repaymentPeriod.addPrincipalAmount(principalDue);
repaymentPeriod.addInterestAmount(interestDue);
repaymentPeriod.setOutstandingLoanBalance(interestRepaymentPeriod.getOutstandingLoanBalance());
scheduleParams.addTotalCumulativePrincipal(principalDue);
scheduleParams.addTotalCumulativeInterest(interestDue);
// add everything
scheduleParams.addTotalRepaymentExpected(principalDue.plus(interestDue));
});
scheduleParams.addTotalCumulativePrincipal(principalDue);
scheduleParams.addTotalCumulativeInterest(interestDue);
// add everything
scheduleParams.addTotalRepaymentExpected(principalDue.plus(interestDue));
});
applyChargesForCurrentPeriod(repaymentPeriod, loanCharges, scheduleParams, currency, mc);
periods.add(repaymentPeriod);
@@ -150,7 +146,8 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
}
if (loanApplicationTerms.isMultiDisburseLoan()) {
processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, null, periods, chargesDueAtTimeOfDisbursement);
processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods,
chargesDueAtTimeOfDisbursement, true);
}
// determine fees and penalties for charges which depends on total
@@ -187,23 +184,23 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
private void processDisbursements(final LoanApplicationTerms loanApplicationTerms,
final ArrayList<DisbursementData> disbursementDataList, final LoanScheduleParams scheduleParams,
final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List<LoanScheduleModelPeriod> periods,
final BigDecimal chargesDueAtTimeOfDisbursement) {
final BigDecimal chargesDueAtTimeOfDisbursement, final boolean includeDisbursementsAfterMaturityDate) {
for (DisbursementData disbursementData : disbursementDataList) {
final LocalDate disbursementDate = disbursementData.disbursementDate();
final LocalDate periodFromDate = scheduleParams.getPeriodStartDate();
final LocalDate periodDueDate = scheduleParams.getActualRepaymentDate();
boolean hasDisbursementAfterLastRepaymentPeriod = interestScheduleModel == null && !disbursementDate.isBefore(periodDueDate);
boolean hasDisbursementInCurrentRepaymentPeriod = interestScheduleModel != null && !disbursementDate.isBefore(periodFromDate)
&& disbursementDate.isBefore(periodDueDate);
final LocalDate maturityDate = interestScheduleModel.getMaturityDate();
boolean hasDisbursementAfterLastRepaymentPeriod = includeDisbursementsAfterMaturityDate
&& !disbursementDate.isBefore(maturityDate);
boolean hasDisbursementInCurrentRepaymentPeriod = !includeDisbursementsAfterMaturityDate
&& !disbursementDate.isBefore(periodFromDate) && disbursementDate.isBefore(periodDueDate);
if (!hasDisbursementAfterLastRepaymentPeriod && !hasDisbursementInCurrentRepaymentPeriod) {
continue;
}
Money outstandingBalance = emiCalculator.findInterestRepaymentPeriod(interestScheduleModel, periodDueDate)
.map(ProgressiveLoanInterestRepaymentModel::getOutstandingBalance)
.orElse(Money.zero(loanApplicationTerms.getCurrency()));
Money outstandingBalance = emiCalculator.getOutstandingLoanBalance(interestScheduleModel, periodDueDate, disbursementDate);
final Money disbursedAmount = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal());
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod
@@ -277,12 +274,11 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
.filter(it -> transactionDate.isAfter(it.getFromDate()) && !transactionDate.isAfter(it.getDueDate())).findFirst()
.orElse(installments.get(0));
ProgressiveLoanInterestRepaymentModel result = emiCalculator
.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate).orElseThrow();
PayableDetails result = emiCalculator.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate);
// TODO: We should add all the past due outstanding amounts as well
OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) //
.principal(result.getOutstandingBalance()) //
.interest(result.getInterestDue());
.interest(result.getPayableInterest());
installments.forEach(installment -> amounts //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
@@ -19,41 +19,43 @@
package org.apache.fineract.portfolio.loanproduct.calc;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
public interface EMICalculator {
ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(List<LoanScheduleModelRepaymentPeriod> periods,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf);
ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc);
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods);
Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel,
LocalDate dueDate);
Optional<RepaymentPeriod> findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate);
void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount);
void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestEffectiveDate,
void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestSubmittedOnDate,
BigDecimal newInterestRate);
void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount);
Optional<ProgressiveLoanInterestRepaymentModel> getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel,
LocalDate periodDueDate, LocalDate payDate);
void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
Money interestAmount);
ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel);
void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
Money principalAmount);
ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel,
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
PayableDetails getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate payDate);
Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate targetDate);
}
@@ -18,110 +18,63 @@
*/
package org.apache.fineract.portfolio.loanproduct.calc;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.Year;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentInterestPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.mapper.ProgressiveLoanInterestRepaymentModelMapper;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@DependsOn("moneyHelper")
public final class ProgressiveEMICalculator implements EMICalculator {
private final ProgressiveLoanInterestRepaymentModelMapper progressiveLoanInterestRepaymentModelMapper;
private static final BigDecimal DIVISOR_100 = new BigDecimal("100");
private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);
@Override
public ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(final List<LoanScheduleModelRepaymentPeriod> periods,
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf) {
final Money zeroAmount = Money.zero(loanProductRelatedDetail.getCurrency());
final ArrayList<ProgressiveLoanInterestRepaymentModel> interestRepaymentModelList = new ArrayList<>(periods.size());
final ArrayList<RepaymentPeriod> interestRepaymentModelList = new ArrayList<>(periods.size());
RepaymentPeriod previousPeriod = null;
for (final LoanScheduleModelRepaymentPeriod period : periods) {
interestRepaymentModelList
.add(new ProgressiveLoanInterestRepaymentModel(period.periodFromDate(), period.periodDueDate(), zeroAmount));
}
if (!interestRepaymentModelList.isEmpty()) {
interestRepaymentModelList.get(interestRepaymentModelList.size() - 1).setLastPeriod(true);
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, period.periodFromDate(), period.periodDueDate(),
zeroAmount);
previousPeriod = currentPeriod;
interestRepaymentModelList.add(currentPeriod);
}
return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail,
installmentAmountInMultiplesOf, mc);
installmentAmountInMultiplesOf);
}
@Override
public Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriod(
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate) {
if (scheduleModel == null || repaymentPeriodDueDate == null) {
public Optional<RepaymentPeriod> findRepaymentPeriod(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate repaymentPeriodDueDate) {
if (scheduleModel == null) {
return Optional.empty();
}
return scheduleModel.repayments().stream()//
.filter(interestRepaymentPeriodItem -> interestRepaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))//
.findFirst();
}
Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriodForBalanceChange(
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate balanceChangeDate) {
if (scheduleModel == null || balanceChangeDate == null) {
return Optional.empty();
}
return scheduleModel.repayments().stream()//
.filter(repaymentPeriod -> !balanceChangeDate.isBefore(repaymentPeriod.getFromDate())
&& (repaymentPeriod.isLastPeriod() || balanceChangeDate.isBefore(repaymentPeriod.getDueDate())))//
.findFirst();
}
Optional<ProgressiveLoanInterestRepaymentInterestPeriod> findInterestPeriodForBalanceChange(
final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate balanceChangeDate) {
if (repaymentPeriod == null || balanceChangeDate == null) {
return Optional.empty();
}
return repaymentPeriod.getInterestPeriods().stream()//
.filter(interestPeriod -> balanceChangeDate.isEqual(interestPeriod.getFromDate()))//
.findFirst();
}
Optional<ProgressiveLoanInterestRepaymentInterestPeriod> findInterestPeriodForInterestChange(
final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate interestRateChangeEffectiveDate) {
if (repaymentPeriod == null || interestRateChangeEffectiveDate == null) {
return Optional.empty();
}
return repaymentPeriod.getInterestPeriods().stream()//
.filter(interestPeriod -> interestRateChangeEffectiveDate.isEqual(interestPeriod.getFromDate()))//
.findFirst();
}
Optional<ProgressiveLoanInterestRepaymentModel> findInterestRepaymentPeriodForInterestChange(
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate interestChangeEffectiveDate) {
if (scheduleModel == null || interestChangeEffectiveDate == null) {
return Optional.empty();
}
return scheduleModel.repayments().stream()//
.filter(repaymentPeriod -> !interestChangeEffectiveDate.isBefore(repaymentPeriod.getFromDate())
&& interestChangeEffectiveDate.isBefore(repaymentPeriod.getDueDate()))//
.findFirst();
return scheduleModel.findRepaymentPeriod(repaymentPeriodDueDate);
}
/**
@@ -130,166 +83,93 @@ public final class ProgressiveEMICalculator implements EMICalculator {
@Override
public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate,
final Money disbursedAmount) {
changeOutstandingBalanceAndUpdateInterestPeriods(scheduleModel, disbursementDueDate, disbursedAmount,
Money.zero(disbursedAmount.getCurrency()))
scheduleModel
.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount,
Money.zero(disbursedAmount.getCurrency()))
.ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel));
}
Optional<ProgressiveLoanInterestRepaymentModel> changeOutstandingBalanceAndUpdateInterestPeriods(
final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate balanceChangeDate, final Money disbursedAmount,
final Money correctionAmount) {
return findInterestRepaymentPeriodForBalanceChange(scheduleModel, balanceChangeDate).stream()//
.peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount, false))//
.findFirst();//
}
@NotNull
private Consumer<ProgressiveLoanInterestRepaymentModel> updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate,
final Money disbursedAmount, final Money correctionAmount, final boolean tillBalanceChangeDate) {
return repaymentPeriod -> {
if (tillBalanceChangeDate && balanceChangeDate.isEqual(repaymentPeriod.getFromDate())) {
insertInterestPeriodIntoStart(repaymentPeriod, disbursedAmount, correctionAmount);
return;
}
final var interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate);
if (interestPeriodOptional.isPresent()) {
interestPeriodOptional.get().addDisbursedAmount(disbursedAmount);
interestPeriodOptional.get().addCorrectionAmount(correctionAmount);
} else {
insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount);
}
};
}
void insertInterestPeriodIntoStart(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final Money disbursedAmount,
final Money correctionAmount) {
final Money zeroAmount = Money.zero(disbursedAmount.getCurrency());
// interestPeriodFromDate is after disb.date because this case when disbursement date is different then interest
// we always have at least one period
final ProgressiveLoanInterestRepaymentInterestPeriod selectedInterestPeriod = repaymentPeriod.getInterestPeriods().get(0);
final LocalDate interestPeriodDueDate = selectedInterestPeriod.getFromDate();
final var newInterestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(selectedInterestPeriod.getFromDate(),
interestPeriodDueDate, BigDecimal.ZERO, zeroAmount, zeroAmount, Money.zero(disbursedAmount.getCurrency()));
newInterestPeriod.setDisbursedAmount(disbursedAmount.add(selectedInterestPeriod.getDisbursedAmount()));
newInterestPeriod.setCorrectionAmount(correctionAmount.add(selectedInterestPeriod.getCorrectionAmount()));
// reset amounts on next periods
selectedInterestPeriod.setDisbursedAmount(zeroAmount);
selectedInterestPeriod.setCorrectionAmount(zeroAmount);
selectedInterestPeriod.setFromDate(interestPeriodDueDate);
repaymentPeriod.getInterestPeriods().add(newInterestPeriod);
Collections.sort(repaymentPeriod.getInterestPeriods());
}
void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate interestPeriodFromDate,
final Money disbursedAmount, final Money correctionAmount) {
// interestPeriodFromDate is after disb.date because this case when disbursement date is different then interest
// period start date
final ProgressiveLoanInterestRepaymentInterestPeriod previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream()
.filter(operationRelatedPreviousInterestPeriod(repaymentPeriod, interestPeriodFromDate))//
.findFirst()//
.get();//
final boolean changeAfterLastRepaymentPeriod = repaymentPeriod.isLastPeriod()
&& previousInterestPeriod.getDueDate().isEqual(repaymentPeriod.getDueDate())
&& !interestPeriodFromDate.isBefore(repaymentPeriod.getDueDate());
final LocalDate interestPeriodDueDate = changeAfterLastRepaymentPeriod ? interestPeriodFromDate.plusDays(1)
: previousInterestPeriod.getDueDate();
final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(interestPeriodFromDate, interestPeriodDueDate,
BigDecimal.ZERO, disbursedAmount, correctionAmount, Money.zero(disbursedAmount.getCurrency()));
previousInterestPeriod.setDueDate(interestPeriodFromDate);
repaymentPeriod.getInterestPeriods().add(interestPeriod);
Collections.sort(repaymentPeriod.getInterestPeriods());
}
private static Predicate<ProgressiveLoanInterestRepaymentInterestPeriod> operationRelatedPreviousInterestPeriod(
ProgressiveLoanInterestRepaymentModel repaymentPeriod, LocalDate operationDate) {
return interestPeriod -> operationDate.isAfter(interestPeriod.getFromDate())
&& (operationDate.isBefore(interestPeriod.getDueDate()) || (repaymentPeriod.getDueDate().equals(interestPeriod.getDueDate())
&& !operationDate.isBefore(repaymentPeriod.getDueDate())));
}
@Override
public ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(final ProgressiveLoanInterestScheduleModel scheduleModel) {
return makeScheduleModelDeepCopy(scheduleModel, scheduleModel.loanProductRelatedDetail(),
scheduleModel.installmentAmountInMultiplesOf(), scheduleModel.mc());
}
@Override
public ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
List<ProgressiveLoanInterestRepaymentModel> repayments = new ArrayList<>(scheduleModel.repayments().size());
for (var repaymentModel : scheduleModel.repayments()) {
repayments.add(new ProgressiveLoanInterestRepaymentModel(repaymentModel));
}
return new ProgressiveLoanInterestScheduleModel(repayments, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
}
@Override
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestEffectiveDate,
public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate,
final BigDecimal newInterestRate) {
final ProgressiveLoanInterestRepaymentModel repaymentPeriod = findInterestRepaymentPeriodForInterestChange(scheduleModel,
newInterestEffectiveDate).orElse(null);
if (repaymentPeriod == null) {
return;
}
scheduleModel.addInterestRate(newInterestEffectiveDate, newInterestRate);
var interestPeriodOptional = findInterestPeriodForInterestChange(repaymentPeriod, newInterestEffectiveDate);
if (interestPeriodOptional.isEmpty()) {
insertInterestPeriod(scheduleModel, repaymentPeriod, newInterestEffectiveDate);
}
calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel);
}
void insertInterestPeriod(final ProgressiveLoanInterestScheduleModel scheduleModel,
final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate interestChangeDueDate) {
// period start date
final ProgressiveLoanInterestRepaymentInterestPeriod previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream()
.filter(interestPeriod -> interestChangeDueDate.isAfter(interestPeriod.getFromDate())
&& interestChangeDueDate.isBefore(interestPeriod.getDueDate()))//
.findFirst()//
.get();//
final Money zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(interestChangeDueDate,
previousInterestPeriod.getDueDate(), BigDecimal.ZERO, zeroAmount, zeroAmount, zeroAmount);
previousInterestPeriod.setDueDate(interestChangeDueDate);
repaymentPeriod.getInterestPeriods().add(interestPeriod);
Collections.sort(repaymentPeriod.getInterestPeriods());
// TODO: impl
}
@Override
public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate,
Money balanceCorrectionAmount) {
final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency());
changeOutstandingBalanceAndUpdateInterestPeriods(scheduleModel, balanceCorrectionDate, zeroAmount, balanceCorrectionAmount)
scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, zeroAmount, balanceCorrectionAmount)
.ifPresent(repaymentPeriod -> {
calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculatePrincipalInterestComponentsForPeriods(scheduleModel);
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
});
}
@Override
public Optional<ProgressiveLoanInterestRepaymentModel> getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate periodDueDate, final LocalDate payDate) {
final var newScheduleModel = makeScheduleModelDeepCopy(scheduleModel);
final var zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
public void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
Money interestAmount) {
findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate).ifPresent(rp -> rp.addPaidInterestAmount(interestAmount));
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
}
return findInterestRepaymentPeriod(newScheduleModel, periodDueDate).stream()
.peek(updateInterestPeriodOnRepaymentPeriod(payDate, zeroAmount, zeroAmount, true))//
.peek(repaymentPeriod -> {
calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel);
calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, payDate);
}).findFirst();
@Override
public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate transactionDate, Money principalAmount) {
findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate).ifPresent(rp -> rp.addPaidPrincipalAmount(principalAmount));
LocalDate balanceCorrectionDate = transactionDate;
if (repaymentPeriodDueDate.isBefore(transactionDate)) {
// If it is paid late, we need to calculate with the period due date
balanceCorrectionDate = repaymentPeriodDueDate;
}
addBalanceCorrection(scheduleModel, balanceCorrectionDate, principalAmount.negated());
}
@Override
public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel,
final LocalDate repaymentPeriodDueDate, final LocalDate targetDate) {
RepaymentPeriod repaymentPeriod = scheduleModel.deepCopy().repaymentPeriods().stream()
.filter(rp -> rp.getDueDate().equals(repaymentPeriodDueDate)).findFirst().orElseThrow();
LocalDate adjustedTargetDate = targetDate;
InterestPeriod interestPeriod;
if (!targetDate.isAfter(repaymentPeriod.getFromDate())) {
interestPeriod = repaymentPeriod.getInterestPeriods().get(0);
adjustedTargetDate = repaymentPeriod.getFromDate();
} else if (targetDate.isAfter(repaymentPeriod.getDueDate())) {
interestPeriod = repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1);
adjustedTargetDate = repaymentPeriod.getDueDate();
} else {
interestPeriod = repaymentPeriod.getInterestPeriods().stream()
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow();
}
interestPeriod.setDueDate(adjustedTargetDate);
int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod);
repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear();
calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel);
// TODO: gather all the unrecognized interest from previous periods based on target date
Money payableInterest = targetDate.isBefore(repaymentPeriod.getFromDate())
? Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency())
: repaymentPeriod.getDueInterest();
Money outstandingLoanBalance = interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount());
Money calculatedEmi = outstandingLoanBalance.plus(payableInterest);
if (calculatedEmi.isLessThan(repaymentPeriod.getEmi())) {
// Review this logic
repaymentPeriod.setEmi(outstandingLoanBalance.plus(payableInterest).plus(repaymentPeriod.getPaidInterest())
.plus(repaymentPeriod.getPaidPrincipal()));
}
Money payablePrincipal = repaymentPeriod.getEmi().minus(payableInterest);
return new PayableDetails(repaymentPeriod.getEmi(), payablePrincipal, payableInterest,
interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount()));
}
@Override
public Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
LocalDate targetDate) {
return getPayableDetails(interestScheduleModel, repaymentPeriodDueDate, targetDate).getOutstandingBalance();
}
/**
@@ -297,56 +177,71 @@ public final class ProgressiveEMICalculator implements EMICalculator {
*/
void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPeriodDueDate,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
final List<ProgressiveLoanInterestRepaymentModel> relatedRepaymentPeriods = getRelatedRepaymentPeriods(
calculateFromRepaymentPeriodDueDate, scheduleModel);
calculateRateFactorMinus1ForPeriods(relatedRepaymentPeriods, scheduleModel);
final List<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate);
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
// TODO: optimalize
calculateOutstandingBalance(scheduleModel);
calculateEMIOnPeriods(relatedRepaymentPeriods, scheduleModel);
calculatePrincipalInterestComponentsForPeriods(scheduleModel);
// TODO: optimalize
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods);
// TODO: optimalize
calculateOutstandingBalance(scheduleModel);
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
}
private static List<ProgressiveLoanInterestRepaymentModel> getRelatedRepaymentPeriods(LocalDate calculateFromRepaymentPeriodDueDate,
ProgressiveLoanInterestScheduleModel scheduleModel) {
return calculateFromRepaymentPeriodDueDate == null ? scheduleModel.repayments()
: scheduleModel.repayments().stream().filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))
.toList();
private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel) {
Money totalDueInterest = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getDueInterest)
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()), Money::plus); // 1.46
Money totalEMI = scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getEmi)
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()), Money::plus); // 101.48
Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream()
.flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount))
.reduce(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()), Money::plus); // 100
Money diff = totalDisbursedAmount.plus(totalDueInterest).minus(totalEMI);
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
.reduce((first, second) -> second);
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff)));
}
private void calculateOutstandingBalance(ProgressiveLoanInterestScheduleModel scheduleModel) {
scheduleModel.repaymentPeriods().forEach(rp -> rp.getInterestPeriods().forEach(InterestPeriod::updateOutstandingLoanBalance));
}
private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel,
final List<ProgressiveLoanInterestRepaymentModel> relatedRepaymentPeriods) {
final List<RepaymentPeriod> relatedRepaymentPeriods) {
final Money emiDifference = getDifferenceBetweenLastTwoPeriod(relatedRepaymentPeriods, scheduleModel);
final int numberOfRelatedPeriods = relatedRepaymentPeriods.size();
double lowerHalfOfRelatedPeriods = Math.floor(numberOfRelatedPeriods / 2.0);
if (emiDifference.isZero() || lowerHalfOfRelatedPeriods == 0.0) {
return;
}
final Money originalEmi = relatedRepaymentPeriods.get(numberOfRelatedPeriods - 2).getEqualMonthlyInstallment();
final Money originalEmi = relatedRepaymentPeriods.get(numberOfRelatedPeriods - 2).getEmi();
boolean shouldBeAdjusted = emiDifference.abs().multipliedBy(100)
.isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods)));
final MathContext mc = scheduleModel.mc();
if (shouldBeAdjusted) {
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc.getRoundingMode());
Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, MoneyHelper.getMathContext().getRoundingMode());
Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel, originalEmi.plus(adjustment));
if (adjustedEqualMonthlyInstallmentValue.compareTo(originalEmi) == 0) {
if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) {
return;
}
final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.get(0).getDueDate();
final var newScheduleModel = makeScheduleModelDeepCopy(scheduleModel);
newScheduleModel.repayments().forEach(period -> {
final ProgressiveLoanInterestScheduleModel newScheduleModel = scheduleModel.deepCopy();
newScheduleModel.repaymentPeriods().forEach(period -> {
if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)) {
period.setEqualMonthlyInstallment(adjustedEqualMonthlyInstallmentValue);
period.setEmi(adjustedEqualMonthlyInstallmentValue);
}
});
calculatePrincipalInterestComponentsForPeriods(newScheduleModel);
final Money newEmiDifference = getDifferenceBetweenLastTwoPeriod(newScheduleModel.repayments(), scheduleModel);
final boolean newEmiHasLessDifference = newEmiDifference.abs().compareTo(emiDifference.abs()) < 0;
final Money newEmiDifference = getDifferenceBetweenLastTwoPeriod(newScheduleModel.repaymentPeriods(), scheduleModel);
final boolean newEmiHasLessDifference = newEmiDifference.abs().isLessThan(emiDifference.abs());
if (!newEmiHasLessDifference) {
return;
}
final Iterator<ProgressiveLoanInterestRepaymentModel> relatedPeriodFromNewModelIterator = newScheduleModel//
.repayments().stream()//
final Iterator<RepaymentPeriod> relatedPeriodFromNewModelIterator = newScheduleModel.repaymentPeriods().stream()//
.filter(period -> !period.getDueDate().isBefore(relatedPeriodsFirstDueDate))//
.toList().iterator();//
@@ -354,12 +249,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
if (!relatedPeriodFromNewModelIterator.hasNext()) {
return;
}
final ProgressiveLoanInterestRepaymentModel newRepaymentPeriod = relatedPeriodFromNewModelIterator.next();
relatedRepaymentPeriod.setEqualMonthlyInstallment(newRepaymentPeriod.getEqualMonthlyInstallment());
relatedRepaymentPeriod.setPrincipalDue(newRepaymentPeriod.getInterestDue());
relatedRepaymentPeriod.setPrincipalDue(newRepaymentPeriod.getPrincipalDue());
relatedRepaymentPeriod.setRemainingBalance(newRepaymentPeriod.getRemainingBalance());
relatedRepaymentPeriod.setInterestPeriods(newRepaymentPeriod.getInterestPeriods());
final RepaymentPeriod newRepaymentPeriod = relatedPeriodFromNewModelIterator.next();
relatedRepaymentPeriod.setEmi(newRepaymentPeriod.getEmi());
});
}
}
@@ -370,36 +261,33 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @param interestRate
* Interest Rate in Percentage
*
* @param mc
*
* @return Rate Interest Rate in fraction format
*/
BigDecimal calcNominalInterestRatePercentage(final BigDecimal interestRate, MathContext mc) {
return MathUtil.nullToZero(interestRate).divide(DIVISOR_100, mc);
BigDecimal calcNominalInterestRatePercentage(final BigDecimal interestRate) {
return MathUtil.nullToZero(interestRate).divide(DIVISOR_100, MoneyHelper.getMathContext());
}
/**
* * Calculate rate factors from ONLY repayment periods
*/
void calculateRateFactorMinus1ForPeriods(final List<ProgressiveLoanInterestRepaymentModel> repaymentPeriods,
void calculateRateFactorForPeriods(final List<RepaymentPeriod> repaymentPeriods,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
repaymentPeriods.forEach(repaymentPeriod -> calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel));
repaymentPeriods.forEach(repaymentPeriod -> calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel));
}
void calculateRateFactorMinus1ForRepaymentPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod,
void calculateRateFactorForRepaymentPeriod(final RepaymentPeriod repaymentPeriod,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
repaymentPeriod.getInterestPeriods().forEach(interestPeriod -> interestPeriod
.setRateFactorMinus1(calculateRateFactorMinus1PerPeriod(repaymentPeriod, interestPeriod, scheduleModel)));
.setRateFactor(calculateRateFactorPerPeriod(repaymentPeriod, interestPeriod, scheduleModel)));
}
/**
* Calculate Rate Factor-1 for an exact Period
* Calculate Rate Factor for an exact Period
*/
BigDecimal calculateRateFactorMinus1PerPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod,
final ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod, final ProgressiveLoanInterestScheduleModel scheduleModel) {
final MathContext mc = scheduleModel.mc();
BigDecimal calculateRateFactorPerPeriod(final RepaymentPeriod repaymentPeriod, final InterestPeriod interestPeriod,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
final LoanProductRelatedDetail loanProductRelatedDetail = scheduleModel.loanProductRelatedDetail();
final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriod.getFromDate()), mc);
final BigDecimal interestRate = calcNominalInterestRatePercentage(scheduleModel.getInterestRate(interestPeriod.getFromDate()));
final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType());
final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType());
final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType();
@@ -418,23 +306,22 @@ public final class ProgressiveEMICalculator implements EMICalculator {
// TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30)
// TODO refactor this logic to represent in interest period
if (partialPeriodCalculationNeeded) {
final BigDecimal cumulatedPeriodFractions = calculatePeriodFractions(interestPeriod, mc);
final BigDecimal cumulatedPeriodFractions = calculatePeriodFractions(interestPeriod);
return rateFactorByRepaymentPartialPeriod(interestRate, repaymentEvery, cumulatedPeriodFractions, BigDecimal.ONE,
BigDecimal.ONE, mc).setScale(mc.getPrecision(), mc.getRoundingMode());
BigDecimal.ONE);
}
return calculateRateFactorMinus1PerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery, daysInMonth,
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());
return calculateRateFactorPerPeriodBasedOnRepaymentFrequency(interestRate, repaymentFrequency, repaymentEvery, daysInMonth,
daysInYear, actualDaysInPeriod, calculatedDaysInPeriod);
}
/**
* Calculate Period fractions part based on how much year has in the period
*
* @param interestPeriod
* @param mc
* @return
*/
BigDecimal calculatePeriodFractions(ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod, MathContext mc) {
BigDecimal calculatePeriodFractions(InterestPeriod interestPeriod) {
BigDecimal cumulatedRateFactor = BigDecimal.ZERO;
int actualYear = interestPeriod.getFromDate().getYear();
int endYear = interestPeriod.getDueDate().getYear();
@@ -445,7 +332,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
endOfActualYear = actualYear == endYear ? interestPeriod.getDueDate() : LocalDate.of(actualYear, 12, 31);
BigDecimal numberOfDaysInYear = BigDecimal.valueOf(Year.of(actualYear).length());
BigDecimal calculatedDaysInActualYear = BigDecimal.valueOf(DateUtils.getDifferenceInDays(actualDate, endOfActualYear));
cumulatedRateFactor = cumulatedRateFactor.add(calculatedDaysInActualYear.divide(numberOfDaysInYear, mc), mc);
cumulatedRateFactor = cumulatedRateFactor
.add(calculatedDaysInActualYear.divide(numberOfDaysInYear, MoneyHelper.getMathContext()), MoneyHelper.getMathContext());
actualDate = endOfActualYear;
actualYear++;
}
@@ -462,48 +350,37 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @param daysInYear
* @param actualDaysInPeriod
* @param calculatedDaysInPeriod
* @param mc
* @return
*/
BigDecimal calculateRateFactorMinus1PerPeriodBasedOnRepaymentFrequency(final BigDecimal interestRate,
BigDecimal calculateRateFactorPerPeriodBasedOnRepaymentFrequency(final BigDecimal interestRate,
final PeriodFrequencyType repaymentFrequency, final BigDecimal repaymentEvery, final BigDecimal daysInMonth,
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
final MathContext mc) {
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
return switch (repaymentFrequency) {
case DAYS -> rateFactorMinus1ByRepaymentEveryDay(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod, mc);
case WEEKS -> rateFactorMinus1ByRepaymentEveryWeek(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod, mc);
case MONTHS -> rateFactorMinus1ByRepaymentEveryMonth(interestRate, repaymentEvery, daysInMonth, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod, mc);
case DAYS ->
rateFactorByRepaymentEveryDay(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod);
case WEEKS ->
rateFactorByRepaymentEveryWeek(interestRate, repaymentEvery, daysInYear, actualDaysInPeriod, calculatedDaysInPeriod);
case MONTHS -> rateFactorByRepaymentEveryMonth(interestRate, repaymentEvery, daysInMonth, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod);
default -> throw new UnsupportedOperationException("Invalid repayment frequency"); // not supported yet
};
}
void calculateEMIOnPeriods(final List<ProgressiveLoanInterestRepaymentModel> repaymentPeriods,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
void calculateEMIOnPeriods(final List<RepaymentPeriod> repaymentPeriods, final ProgressiveLoanInterestScheduleModel scheduleModel) {
if (repaymentPeriods.isEmpty()) {
return;
}
final MathContext mc = scheduleModel.mc();
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorN(repaymentPeriods, mc));
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods, mc));
final var startPeriod = repaymentPeriods.get(0);
final Money remainingBalanceFromPreviousPeriod = getRemainingBalanceFromPreviousPeriod(scheduleModel, startPeriod);
final Money outstandingBalance = remainingBalanceFromPreviousPeriod.add(startPeriod.getDisbursedAmountInPeriod());
final BigDecimal rateFactorN = MathUtil.stripTrailingZeros(calculateRateFactorPlus1N(repaymentPeriods));
final BigDecimal fnResult = MathUtil.stripTrailingZeros(calculateFnResult(repaymentPeriods));
final RepaymentPeriod startPeriod = repaymentPeriods.get(0);
// TODO: double check
final Money outstandingBalance = startPeriod.getInitialBalanceForEmiRecalculation();
final Money equalMonthlyInstallment = Money.of(outstandingBalance.getCurrency(),
calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult, mc));
calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult));
final Money finalEqualMonthlyInstallment = applyInstallmentAmountInMultiplesOf(scheduleModel, equalMonthlyInstallment);
repaymentPeriods.forEach(period -> period.setEqualMonthlyInstallment(finalEqualMonthlyInstallment));
}
Money getRemainingBalanceFromPreviousPeriod(ProgressiveLoanInterestScheduleModel scheduleModel,
ProgressiveLoanInterestRepaymentModel startPeriod) {
return scheduleModel.repayments().stream().filter(period -> period.getDueDate().isEqual(startPeriod.getFromDate()))
.map(ProgressiveLoanInterestRepaymentModel::getRemainingBalance).findFirst()
.orElse(Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()));
repaymentPeriods.forEach(period -> period.setEmi(finalEqualMonthlyInstallment));
}
Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleModel scheduleModel,
@@ -513,41 +390,41 @@ public final class ProgressiveEMICalculator implements EMICalculator {
: equalMonthlyInstallment;
}
Money getDifferenceBetweenLastTwoPeriod(final List<ProgressiveLoanInterestRepaymentModel> repaymentPeriods,
Money getDifferenceBetweenLastTwoPeriod(final List<RepaymentPeriod> repaymentPeriods,
final ProgressiveLoanInterestScheduleModel scheduleModel) {
int numberOfUpcomingPeriods = repaymentPeriods.size();
if (numberOfUpcomingPeriods < 2) {
return Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
}
final var lastPeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 1);
final var penultimatePeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 2);
return lastPeriod.getEqualMonthlyInstallment().minus(penultimatePeriod.getEqualMonthlyInstallment());
final RepaymentPeriod lastPeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 1);
final RepaymentPeriod penultimatePeriod = repaymentPeriods.get(numberOfUpcomingPeriods - 2);
return lastPeriod.getEmi().minus(penultimatePeriod.getEmi());
}
/**
* Calculate Rate Factor Product from rate factors
*/
BigDecimal calculateRateFactorN(final List<ProgressiveLoanInterestRepaymentModel> periods, final MathContext mc) {
return periods.stream().map(ProgressiveLoanInterestRepaymentModel::getRateFactor).reduce(BigDecimal.ONE,
(BigDecimal acc, BigDecimal value) -> acc.multiply(value, mc));
BigDecimal calculateRateFactorPlus1N(final List<RepaymentPeriod> periods) {
return periods.stream().map(RepaymentPeriod::getRateFactorPlus1).reduce(BigDecimal.ONE,
(BigDecimal acc, BigDecimal value) -> acc.multiply(value, MoneyHelper.getMathContext()));
}
/**
* Summarize Fn values
*/
BigDecimal calculateFnResult(final List<ProgressiveLoanInterestRepaymentModel> periods, final MathContext mc) {
BigDecimal calculateFnResult(final List<RepaymentPeriod> periods) {
return periods.stream()//
.skip(1)//
.map(ProgressiveLoanInterestRepaymentModel::getRateFactor)//
.reduce(BigDecimal.ONE, (BigDecimal previousValue, BigDecimal rateFactor) -> fnValue(previousValue, rateFactor, mc));//
.map(RepaymentPeriod::getRateFactorPlus1)//
.reduce(BigDecimal.ONE, this::fnValue);//
}
/**
* Calculate the EMI (Equal Monthly Installment) value
*/
BigDecimal calculateEMIValue(final BigDecimal rateFactorN, final BigDecimal outstandingBalanceForRest, final BigDecimal fnResult,
final MathContext mc) {
return rateFactorN.multiply(outstandingBalanceForRest, mc).divide(fnResult, mc);
BigDecimal calculateEMIValue(final BigDecimal rateFactorPlus1N, final BigDecimal outstandingBalanceForRest, final BigDecimal fnResult) {
return rateFactorPlus1N.multiply(outstandingBalanceForRest, MoneyHelper.getMathContext()).divide(fnResult,
MoneyHelper.getMathContext());
}
/**
@@ -572,14 +449,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @param calculatedDaysInPeriod
* Calculated days in Period (It has importance related to Reschedule)
*
* @param mc
* @return Rate Factor for period
*/
BigDecimal rateFactorMinus1ByRepaymentEveryDay(final BigDecimal interestRate, final BigDecimal repaymentEvery,
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
final MathContext mc) {
return rateFactorMinus1ByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod, mc);
BigDecimal rateFactorByRepaymentEveryDay(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal daysInYear,
final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod);
}
/**
@@ -604,14 +479,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @param calculatedDaysInPeriod
* Calculated days in Period (It has importance related to Reschedule)
*
* @param mc
* @return Rate Factor for period
*/
BigDecimal rateFactorMinus1ByRepaymentEveryWeek(final BigDecimal interestRate, final BigDecimal repaymentEvery,
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
final MathContext mc) {
return rateFactorMinus1ByRepaymentPeriod(interestRate, ONE_WEEK_IN_DAYS, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod, mc);
BigDecimal rateFactorByRepaymentEveryWeek(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal daysInYear,
final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
return rateFactorByRepaymentPeriod(interestRate, ONE_WEEK_IN_DAYS, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod);
}
/**
@@ -639,14 +512,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @param calculatedDaysInPeriod
* Calculated days in Period (It has importance related to Reschedule)
*
* @param mc
* @return Rate Factor for period
*/
BigDecimal rateFactorMinus1ByRepaymentEveryMonth(final BigDecimal interestRate, final BigDecimal repaymentEvery,
final BigDecimal daysInMonth, final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
return rateFactorMinus1ByRepaymentPeriod(interestRate, daysInMonth, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod, mc);
BigDecimal rateFactorByRepaymentEveryMonth(final BigDecimal interestRate, final BigDecimal repaymentEvery, final BigDecimal daysInMonth,
final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
return rateFactorByRepaymentPeriod(interestRate, daysInMonth, repaymentEvery, daysInYear, actualDaysInPeriod,
calculatedDaysInPeriod);
}
/**
@@ -674,17 +545,18 @@ public final class ProgressiveEMICalculator implements EMICalculator {
* @param calculatedDaysInPeriod
* Calculated days in Period (It has importance related to Reschedule)
*
* @param mc
* @return Rate Factor for period
*/
BigDecimal rateFactorMinus1ByRepaymentPeriod(final BigDecimal interestRate, final BigDecimal repaymentPeriodMultiplierInDays,
BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigDecimal repaymentPeriodMultiplierInDays,
final BigDecimal repaymentEvery, final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
final BigDecimal interestFractionPerPeriod = repaymentPeriodMultiplierInDays.multiply(repaymentEvery, mc).divide(daysInYear, mc);
final BigDecimal calculatedDaysInPeriod) {
final BigDecimal interestFractionPerPeriod = repaymentPeriodMultiplierInDays//
.multiply(repaymentEvery, MoneyHelper.getMathContext())//
.divide(daysInYear, MoneyHelper.getMathContext());//
return interestRate//
.multiply(interestFractionPerPeriod, mc)//
.multiply(actualDaysInPeriod, mc)//
.divide(calculatedDaysInPeriod, mc);//
.multiply(interestFractionPerPeriod, MoneyHelper.getMathContext())//
.multiply(actualDaysInPeriod, MoneyHelper.getMathContext())//
.divide(calculatedDaysInPeriod, MoneyHelper.getMathContext());//
}
/**
@@ -692,13 +564,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
*
*/
BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, final BigDecimal repaymentEvery,
final BigDecimal cumulatedPeriodRatio, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
final MathContext mc) {
final BigDecimal interestFractionPerPeriod = repaymentEvery.multiply(cumulatedPeriodRatio, mc);
final BigDecimal cumulatedPeriodRatio, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod) {
final BigDecimal interestFractionPerPeriod = repaymentEvery.multiply(cumulatedPeriodRatio);
return interestRate//
.multiply(interestFractionPerPeriod, mc)//
.multiply(actualDaysInPeriod, mc)//
.divide(calculatedDaysInPeriod, mc);//
.multiply(interestFractionPerPeriod, MoneyHelper.getMathContext())//
.multiply(actualDaysInPeriod, MoneyHelper.getMathContext())//
.divide(calculatedDaysInPeriod, MoneyHelper.getMathContext());//
}
/**
@@ -710,67 +581,26 @@ public final class ProgressiveEMICalculator implements EMICalculator {
*
* @param currentRateFactor
*
* @param mc
*
*/
BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRateFactor, final MathContext mc) {
return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc), mc);
}
void calculatePrincipalInterestComponentsForPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel) {
Money outstandingBalance = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency());
for (var repaymentPeriod : scheduleModel.repayments()) {
repaymentPeriod.setInitialBalance(outstandingBalance);
calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, null);
outstandingBalance = repaymentPeriod.getRemainingBalance();
}
}
void calculatePrincipalInterestComponentsForPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod,
final LocalDate calculateTill) {
final Money zeroAmount = Money.zero(repaymentPeriod.getInitialBalance().getCurrency());
Money outstandingBalance = repaymentPeriod.getInitialBalance();
Money balanceCorrection = zeroAmount;
Money cumulatedInterest = zeroAmount;
for (ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) {
final boolean shouldInvalidateInterestPeriod = calculateTill != null && interestPeriod.getDueDate().isAfter(calculateTill);
if (shouldInvalidateInterestPeriod) {
interestPeriod.setInterestDue(zeroAmount);
interestPeriod.setDisbursedAmount(zeroAmount);
interestPeriod.setCorrectionAmount(zeroAmount);
continue;
}
outstandingBalance = outstandingBalance.plus(interestPeriod.getDisbursedAmount());
balanceCorrection = balanceCorrection.plus(interestPeriod.getCorrectionAmount());
final Money calculatedInterest = outstandingBalance.plus(balanceCorrection).multipliedBy(interestPeriod.getRateFactorMinus1());
interestPeriod.setInterestDue(calculatedInterest);
cumulatedInterest = cumulatedInterest.plus(calculatedInterest);
}
final Money calculatedPrincipal = repaymentPeriod.isLastPeriod() ? outstandingBalance
: repaymentPeriod.getEqualMonthlyInstallment().minus(cumulatedInterest);
if (repaymentPeriod.isLastPeriod()) {
repaymentPeriod.setEqualMonthlyInstallment(calculatedPrincipal.add(cumulatedInterest));
}
final Money remainingBalance = outstandingBalance.minus(calculatedPrincipal);
repaymentPeriod.setPrincipalDue(calculatedPrincipal);
repaymentPeriod.setRemainingBalance(remainingBalance);
BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRateFactor) {
return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, MoneyHelper.getMathContext()), MoneyHelper.getMathContext());
}
@Override
public ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc) {
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods) {
List<LoanRepaymentScheduleInstallment> repaymentModelsWithoutDownPayment = repaymentPeriods.stream()
.filter(period -> !period.isDownPayment() && !period.isAdditional()).toList();
List<ProgressiveLoanInterestRepaymentModel> repaymentModels = progressiveLoanInterestRepaymentModelMapper
.map(repaymentModelsWithoutDownPayment);
if (!repaymentModels.isEmpty()) {
repaymentModels.get(repaymentModels.size() - 1).setLastPeriod(true);
List<RepaymentPeriod> repaymentModels = new ArrayList<>();
RepaymentPeriod previousPeriod = null;
for (LoanRepaymentScheduleInstallment repaymentModel : repaymentModelsWithoutDownPayment) {
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentModel.getFromDate(), repaymentModel.getDueDate(),
Money.zero(repaymentModel.getLoan().getCurrency()));
previousPeriod = currentPeriod;
repaymentModels.add(currentPeriod);
}
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf);
}
}
@@ -1,37 +0,0 @@
/**
* 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.mapper;
import java.util.List;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel;
import org.mapstruct.Mapper;
@Mapper(config = MapstructMapperConfig.class)
public interface ProgressiveLoanInterestRepaymentModelMapper {
List<ProgressiveLoanInterestRepaymentModel> map(List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments);
default ProgressiveLoanInterestRepaymentModel map(LoanRepaymentScheduleInstallment repaymentScheduleInstallment) {
return new ProgressiveLoanInterestRepaymentModel(repaymentScheduleInstallment.getFromDate(),
repaymentScheduleInstallment.getDueDate(), Money.zero(repaymentScheduleInstallment.getLoan().getCurrency()));
}
}
@@ -47,7 +47,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class LoanScheduleGeneratorTest {
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(null);
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$");
private static final MonetaryCurrency MONETARY_CURRENCY = MonetaryCurrency.fromApplicationCurrency(APPLICATION_CURRENCY);