FINERACT-1981: EMI calculator rework
This commit is contained in:
committed by
Adam Saghy
parent
d4190ede90
commit
bb072709b5
+10
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -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 | |
|
||||
|
||||
+123
-77
@@ -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(),
|
||||
|
||||
+106
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -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());
|
||||
}
|
||||
}
|
||||
+8
-21
@@ -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;
|
||||
}
|
||||
-61
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-99
@@ -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());
|
||||
}
|
||||
}
|
||||
+140
-18
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+152
@@ -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);
|
||||
}
|
||||
}
|
||||
+31
-35
@@ -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))
|
||||
|
||||
+14
-12
@@ -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);
|
||||
}
|
||||
|
||||
+211
-381
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
-37
@@ -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()));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+279
-394
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user