From bb072709b50f222746ecd3b66c666e4c83b737b3 Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Tue, 1 Oct 2024 15:58:30 +0200 Subject: [PATCH] FINERACT-1981: EMI calculator rework --- .../infrastructure/core/service/MathUtil.java | 10 + .../LoanProductGlobalInitializerStep.java | 5 +- .../src/test/resources/features/Loan.feature | 4 +- ...edPaymentScheduleTransactionProcessor.java | 200 ++++-- .../loanschedule/data/InterestPeriod.java | 106 +++ ...oanInterestRate.java => InterestRate.java} | 8 +- .../loanschedule/data/PayableDetails.java} | 29 +- ...veLoanInterestRepaymentInterestPeriod.java | 61 -- ...ProgressiveLoanInterestRepaymentModel.java | 99 --- .../ProgressiveLoanInterestScheduleModel.java | 158 +++- .../loanschedule/data/RepaymentPeriod.java | 152 ++++ .../ProgressiveLoanScheduleGenerator.java | 66 +- .../loanproduct/calc/EMICalculator.java | 26 +- .../calc/ProgressiveEMICalculator.java | 586 ++++++--------- ...ssiveLoanInterestRepaymentModelMapper.java | 37 - .../domain/LoanScheduleGeneratorTest.java | 2 +- .../calc/ProgressiveEMICalculatorTest.java | 675 ++++++++---------- 17 files changed, 1080 insertions(+), 1144 deletions(-) create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/{ProgressiveLoanInterestRate.java => InterestRate.java} (81%) rename fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/{loanproduct/calc/EMICalculationResult.java => loanaccount/loanschedule/data/PayableDetails.java} (54%) delete mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java delete mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java delete mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/ProgressiveLoanInterestRepaymentModelMapper.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java index f9c2bdcc372..406f16c0856 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java @@ -481,4 +481,14 @@ public static BigDecimal percentageOf(final BigDecimal value, final BigDecimal p 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; + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java index c4f5125d00d..24e51f80d05 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java @@ -497,10 +497,7 @@ public void initialize() throws Exception { .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 responseLoanProductsRequestLP2AdvancedpaymentInterest36030InterestRecalcTillPreClose = loanProductsApi .createLoanProduct(loanProductsRequestLP2AdvancedpaymentInterestEmi36030InterestRecalcTillPreclose).execute(); TestContext.INSTANCE.set( diff --git a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature index 938813a1c12..7b8fc14e2be 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/Loan.feature @@ -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 | | \ No newline at end of file + | 4 | loanTermType.dueDate | dueDate | 01 February 2023 | 50.0 | 15 January 2023 | false | | diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 8a9394baf68..32f44df2595 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -79,7 +79,7 @@ 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 Pair repr List 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 Pair repr LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId); createNewTransaction(oldTransaction, newTransaction, ctx); } - recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx, true); + recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx); List txs = chargeOrTransactions.stream() // .filter(ChargeOrTransaction::isTransaction) // .map(e -> e.getLoanTransaction().get()).toList(); @@ -778,14 +777,14 @@ private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTra 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 @@ private List findOverdueInstallmentsBeforeDate .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 @@ private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransa 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 @@ private void adjustOverduePrincipalForInstallment(LocalDate currentDate, boolean 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 @@ private void adjustOverduePrincipalForInstallment(LocalDate currentDate, boolean // 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 @@ private void adjustOverduePrincipalForInstallment(LocalDate currentDate, boolean } 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 @@ private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactio 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 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr 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 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr 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 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr 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 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Tr return transactionAmountUnprocessed; } + private Money handlingPaymentAllocationForInterestBearingProgressiveLoan(LoanTransaction loanTransaction, + Money transactionAmountUnprocessed, Balances balances, PaymentAllocationType paymentAllocationType, + LoanRepaymentScheduleInstallment installment, ProgressiveTransactionCtx ctx, + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set 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 getLoanChargesOfInstallment(Set charges, LoanRepaymentScheduleInstallment currentInstallment, + private Set getLoanChargesOfInstallment(Set charges, LoanRepaymentScheduleInstallment currentInstallment, int firstNormalInstallmentNumber) { return charges.stream().filter(loanCharge -> currentInstallment.getInstallmentNumber().equals(firstNormalInstallmentNumber) ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(currentInstallment.getFromDate(), diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java new file mode 100644 index 00000000000..85366e602f9 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java @@ -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 { + + 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 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); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRate.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java similarity index 81% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRate.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java index 76c85bc5408..bf5fe3474ce 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRate.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java @@ -22,11 +22,13 @@ import java.time.LocalDate; import org.jetbrains.annotations.NotNull; -public record ProgressiveLoanInterestRate(LocalDate effectiveFrom, LocalDate validFrom, - BigDecimal interestRate) implements Comparable { +public record InterestRate(LocalDate effectiveFrom, // + LocalDate validFrom, // + BigDecimal interestRate // +) implements Comparable { @Override - public int compareTo(@NotNull ProgressiveLoanInterestRate o) { + public int compareTo(@NotNull InterestRate o) { return this.effectiveFrom().compareTo(o.effectiveFrom()); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PayableDetails.java similarity index 54% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PayableDetails.java index 1990f6355b7..dd131e205a3 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculationResult.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PayableDetails.java @@ -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 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; } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java deleted file mode 100644 index 42235c756a0..00000000000 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java +++ /dev/null @@ -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 { - - 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); - } - } -} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java deleted file mode 100644 index 7d1fa31077e..00000000000 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java +++ /dev/null @@ -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 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()); - } -} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java index 9b79eebcf2d..45f61c02f46 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java @@ -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 repayments, // - List interestRates, // - LoanProductRelatedDetail loanProductRelatedDetail, // - Integer installmentAmountInMultiplesOf, // - MathContext mc) { +@Data +@Accessors(fluent = true) +public class ProgressiveLoanInterestScheduleModel { - public ProgressiveLoanInterestScheduleModel(List repayments, - LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) { - this(repayments, new ArrayList<>(1), loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + private final List repaymentPeriods; + private final List interestRates; + private final LoanProductRelatedDetail loanProductRelatedDetail; + private final Integer installmentAmountInMultiplesOf; + + public ProgressiveLoanInterestScheduleModel(List repaymentPeriods, LoanProductRelatedDetail loanProductRelatedDetail, + Integer installmentAmountInMultiplesOf) { + this.repaymentPeriods = repaymentPeriods; + this.interestRates = new ArrayList<>(); + this.loanProductRelatedDetail = loanProductRelatedDetail; + this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf; + } + + private ProgressiveLoanInterestScheduleModel(List repaymentPeriods, final List 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); } - public void addInterestRate(final LocalDate newInterestDueDate, final BigDecimal newInterestRate) { - interestRates.add(new ProgressiveLoanInterestRate(newInterestDueDate, newInterestDueDate.plusDays(1), newInterestRate)); - interestRates.sort(Collections.reverseOrder()); + private List copyRepaymentPeriods(final List repaymentPeriods) { + final List 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 BigDecimal getInterestRate(final LocalDate effectiveDate) { } 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 findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) { + if (repaymentPeriodDueDate == null) { + return Optional.empty(); + } + return repaymentPeriods.stream()// + .filter(repaymentPeriodItem -> repaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))// + .findFirst(); + } + + public List 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 changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate, + final Money disbursedAmount, final Money correctionAmount) { + return findRepaymentPeriodForBalanceChange(balanceChangeDate).stream()// + .peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount))// + .findFirst();// + } + + Optional 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 updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount, + final Money correctionAmount) { + return repaymentPeriod -> { + final Optional interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate); + if (interestPeriodOptional.isPresent()) { + interestPeriodOptional.get().addDisbursementAmount(disbursedAmount); + interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount); + } else { + insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount); + } + }; + } + + Optional 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()); + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java new file mode 100644 index 00000000000..008f8c39c6d --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java @@ -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 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 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); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index 2824d91f7ce..2de29d2c969 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -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.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 LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer } // 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 LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer .generateRepaymentPeriods(periodStartDate, loanApplicationTerms, holidayDetailDTO); final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel( expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetail(), - loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc); - final List periods = new ArrayList<>(expectedRepaymentPeriods.size() + 2); + loanApplicationTerms.getInstallmentAmountInMultiplesOf()); + final List periods = new ArrayList<>(expectedRepaymentPeriods.size()); prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms); @@ -113,34 +110,33 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer 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 LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer } 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 @@ private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTer private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final ArrayList disbursementDataList, final LoanScheduleParams scheduleParams, final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List 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 OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency .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)) diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java index 4ca096f272a..157989912ac 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -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 periods, - LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc); + LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf); ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail, - Integer installmentAmountInMultiplesOf, List repaymentPeriods, MathContext mc); + Integer installmentAmountInMultiplesOf, List repaymentPeriods); - Optional findInterestRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, - LocalDate dueDate); + Optional 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 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); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index 390052799d2..a632654f608 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -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 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 interestRepaymentModelList = new ArrayList<>(periods.size()); + final ArrayList 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 findInterestRepaymentPeriod( - final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate) { - if (scheduleModel == null || repaymentPeriodDueDate == null) { - return Optional.empty(); - } - return scheduleModel.repayments().stream()// - .filter(interestRepaymentPeriodItem -> interestRepaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))// - .findFirst(); - } - - Optional 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 findInterestPeriodForBalanceChange( - final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate balanceChangeDate) { - if (repaymentPeriod == null || balanceChangeDate == null) { + public Optional findRepaymentPeriod(final ProgressiveLoanInterestScheduleModel scheduleModel, + final LocalDate repaymentPeriodDueDate) { + if (scheduleModel == null) { return Optional.empty(); } - return repaymentPeriod.getInterestPeriods().stream()// - .filter(interestPeriod -> balanceChangeDate.isEqual(interestPeriod.getFromDate()))// - .findFirst(); - } - - Optional 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 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 @@ Optional findInterestRepaymentPeriodForIn @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 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 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()); + @Override + public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate, + final BigDecimal newInterestRate) { + // TODO: impl } - private static Predicate 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 void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate, + Money balanceCorrectionAmount) { + final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency()); + scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, zeroAmount, balanceCorrectionAmount) + .ifPresent(repaymentPeriod -> { + calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); + calculateOutstandingBalance(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + }); } @Override - public ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(final ProgressiveLoanInterestScheduleModel scheduleModel) { - return makeScheduleModelDeepCopy(scheduleModel, scheduleModel.loanProductRelatedDetail(), - scheduleModel.installmentAmountInMultiplesOf(), scheduleModel.mc()); + public void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate, + Money interestAmount) { + findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate).ifPresent(rp -> rp.addPaidInterestAmount(interestAmount)); + calculateOutstandingBalance(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); } @Override - public ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(final ProgressiveLoanInterestScheduleModel scheduleModel, - final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) { - List repayments = new ArrayList<>(scheduleModel.repayments().size()); - for (var repaymentModel : scheduleModel.repayments()) { - repayments.add(new ProgressiveLoanInterestRepaymentModel(repaymentModel)); + 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; } - return new ProgressiveLoanInterestScheduleModel(repayments, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + addBalanceCorrection(scheduleModel, balanceCorrectionDate, principalAmount.negated()); } @Override - public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestEffectiveDate, - final BigDecimal newInterestRate) { - final ProgressiveLoanInterestRepaymentModel repaymentPeriod = findInterestRepaymentPeriodForInterestChange(scheduleModel, - newInterestEffectiveDate).orElse(null); - if (repaymentPeriod == null) { - return; + 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(); } - scheduleModel.addInterestRate(newInterestEffectiveDate, newInterestRate); - var interestPeriodOptional = findInterestPeriodForInterestChange(repaymentPeriod, newInterestEffectiveDate); - if (interestPeriodOptional.isEmpty()) { - insertInterestPeriod(scheduleModel, repaymentPeriod, newInterestEffectiveDate); + 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())); } - - 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()); + Money payablePrincipal = repaymentPeriod.getEmi().minus(payableInterest); + return new PayableDetails(repaymentPeriod.getEmi(), payablePrincipal, payableInterest, + interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount())); } @Override - public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate, - Money balanceCorrectionAmount) { - final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency()); - changeOutstandingBalanceAndUpdateInterestPeriods(scheduleModel, balanceCorrectionDate, zeroAmount, balanceCorrectionAmount) - .ifPresent(repaymentPeriod -> { - calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel); - calculatePrincipalInterestComponentsForPeriods(scheduleModel); - }); - } - - @Override - public Optional getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel, - final LocalDate periodDueDate, final LocalDate payDate) { - final var newScheduleModel = makeScheduleModelDeepCopy(scheduleModel); - final var zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()); - - return findInterestRepaymentPeriod(newScheduleModel, periodDueDate).stream() - .peek(updateInterestPeriodOnRepaymentPeriod(payDate, zeroAmount, zeroAmount, true))// - .peek(repaymentPeriod -> { - calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel); - calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, payDate); - }).findFirst(); + public Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate, + LocalDate targetDate) { + return getPayableDetails(interestScheduleModel, repaymentPeriodDueDate, targetDate).getOutstandingBalance(); } /** @@ -297,56 +177,71 @@ public Optional getPayableDetails(final P */ void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPeriodDueDate, final ProgressiveLoanInterestScheduleModel scheduleModel) { - final List relatedRepaymentPeriods = getRelatedRepaymentPeriods( - calculateFromRepaymentPeriodDueDate, scheduleModel); - calculateRateFactorMinus1ForPeriods(relatedRepaymentPeriods, scheduleModel); + final List 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 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 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 relatedRepaymentPeriods) { + final List 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 relatedPeriodFromNewModelIterator = newScheduleModel// - .repayments().stream()// + final Iterator relatedPeriodFromNewModelIterator = newScheduleModel.repaymentPeriods().stream()// .filter(period -> !period.getDueDate().isBefore(relatedPeriodsFirstDueDate))// .toList().iterator();// @@ -354,12 +249,8 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv 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 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv * @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 repaymentPeriods, + void calculateRateFactorForPeriods(final List 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 @@ BigDecimal calculateRateFactorMinus1PerPeriod(final ProgressiveLoanInterestRepay // 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 @@ BigDecimal calculatePeriodFractions(ProgressiveLoanInterestRepaymentInterestPeri 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 @@ BigDecimal calculatePeriodFractions(ProgressiveLoanInterestRepaymentInterestPeri * @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 repaymentPeriods, - final ProgressiveLoanInterestScheduleModel scheduleModel) { + void calculateEMIOnPeriods(final List 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 @@ Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleM : equalMonthlyInstallment; } - Money getDifferenceBetweenLastTwoPeriod(final List repaymentPeriods, + Money getDifferenceBetweenLastTwoPeriod(final List 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 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 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 periods, final MathContext mc) { + BigDecimal calculateFnResult(final List 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 @@ BigDecimal calculateEMIValue(final BigDecimal rateFactorN, final BigDecimal outs * @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 @@ BigDecimal rateFactorMinus1ByRepaymentEveryDay(final BigDecimal interestRate, fi * @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 @@ BigDecimal rateFactorMinus1ByRepaymentEveryWeek(final BigDecimal interestRate, f * @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 @@ BigDecimal rateFactorMinus1ByRepaymentEveryMonth(final BigDecimal interestRate, * @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 @@ BigDecimal rateFactorMinus1ByRepaymentPeriod(final BigDecimal interestRate, fina * */ 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 @@ BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, fin * * @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 repaymentPeriods, MathContext mc) { + Integer installmentAmountInMultiplesOf, List repaymentPeriods) { List repaymentModelsWithoutDownPayment = repaymentPeriods.stream() .filter(period -> !period.isDownPayment() && !period.isAdditional()).toList(); - List repaymentModels = progressiveLoanInterestRepaymentModelMapper - .map(repaymentModelsWithoutDownPayment); - if (!repaymentModels.isEmpty()) { - repaymentModels.get(repaymentModels.size() - 1).setLastPeriod(true); + List 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); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/ProgressiveLoanInterestRepaymentModelMapper.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/ProgressiveLoanInterestRepaymentModelMapper.java deleted file mode 100644 index a234197a3c3..00000000000 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/mapper/ProgressiveLoanInterestRepaymentModelMapper.java +++ /dev/null @@ -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 map(List repaymentScheduleInstallments); - - default ProgressiveLoanInterestRepaymentModel map(LoanRepaymentScheduleInstallment repaymentScheduleInstallment) { - return new ProgressiveLoanInterestRepaymentModel(repaymentScheduleInstallment.getFromDate(), - repaymentScheduleInstallment.getDueDate(), Money.zero(repaymentScheduleInstallment.getLoan().getCurrency())); - } -} diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java index 7f8f3e1ae72..b0015c29c94 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java @@ -47,7 +47,7 @@ @ExtendWith(MockitoExtension.class) class LoanScheduleGeneratorTest { - private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(null); + private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(); private static MockedStatic 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); diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index a769e5b7439..6aebb4ad593 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -34,14 +34,17 @@ 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.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.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.MockedStatic; @@ -51,7 +54,7 @@ @ExtendWith(MockitoExtension.class) class ProgressiveEMICalculatorTest { - private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(null); + private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(); private static MockedStatic threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class); private static MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); @@ -80,7 +83,8 @@ public static void init() { } @AfterAll - public static void destruct() { + public static void tearDown() { + threadLocalContextUtil.close(); moneyHelper.close(); } @@ -89,8 +93,9 @@ private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, fi final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate())); final BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(period.getFromDate())); final BigDecimal daysInMonth = BigDecimal.valueOf(daysInMonthType.getNumberOfDays(period.getFromDate())); - return emiCalculator.rateFactorMinus1ByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, daysInPeriod, - daysInPeriod, MoneyHelper.getMathContext()); + final BigDecimal rateFactor = emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, + daysInPeriod, daysInPeriod); + return rateFactor.setScale(12, MoneyHelper.getRoundingMode()); } @Test @@ -98,13 +103,12 @@ public void testRateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthAc // Given final DaysInYearType daysInYearType = DaysInYearType.DAYS_365; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; - final MathContext mc = MoneyHelper.getMathContext(); - final String[] expectedValues = new String[] { "1.00805337534", "1.00753380274", "1.00805337534", "1.00779358904", "1.00805337534", - "1.00779358904" }; + final String[] expectedValues = new String[] { "0.008053375342", "0.007533802740", "0.008053375342", "0.007793589041", + "0.008053375342", "0.007793589041" }; // Then for (LoanRepaymentScheduleInstallment period : periods) { - BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period).add(BigDecimal.ONE, mc); + BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period); Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], rateFactor.toString()); } } @@ -114,13 +118,13 @@ public void testRateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMont // Given final DaysInYearType daysInYearType = DaysInYearType.ACTUAL; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; - final MathContext mc = MoneyHelper.getMathContext(); - final String[] expectedValues = new String[] { "1.00803137158", "1.00751321858", "1.00803137158", "1.00777229508", "1.00803137158", - "1.00777229508" }; + + final String[] expectedValues = new String[] { "0.008031371585", "0.007513218579", "0.008031371585", "0.007772295082", + "0.008031371585", "0.007772295082" }; // Then for (LoanRepaymentScheduleInstallment period : periods) { - BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period).add(BigDecimal.ONE, mc); + BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period); Assertions.assertEquals(expectedValues[period.getInstallmentNumber() - 1], rateFactor.toString()); } } @@ -130,16 +134,17 @@ public void testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual( // Given final DaysInYearType daysInYearType = DaysInYearType.DAYS_365; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; - final MathContext mc = MoneyHelper.getMathContext(); + final String[] expectedValues = new String[] { "1.00000000000", "2.00753380274", "3.02370122596", "4.04726671069", "5.07986086861", "6.11945121660" }; final List fnValuesCalculated = new ArrayList<>(); BigDecimal previousFnValue = BigDecimal.ZERO; for (LoanRepaymentScheduleInstallment period : periods) { - BigDecimal rateFactor = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period).add(BigDecimal.ONE, mc); + BigDecimal rateFactorPlus1 = getRateFactorsByMonth(daysInYearType, daysInMonthType, interestRate, period).add(BigDecimal.ONE, + MoneyHelper.getMathContext()); - final BigDecimal currentFnValue = emiCalculator.fnValue(previousFnValue, rateFactor, mc); + final BigDecimal currentFnValue = emiCalculator.fnValue(previousFnValue, rateFactorPlus1); fnValuesCalculated.add(currentFnValue); previousFnValue = currentFnValue; @@ -153,7 +158,7 @@ public void testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual( @Test public void testEMICalculator_generateInterestScheduleModel() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); final Integer installmentAmountInMultiplesOf = null; @@ -165,20 +170,19 @@ public void testEMICalculator_generateInterestScheduleModel() { Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator - .generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + .generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf); Assertions.assertTrue(interestScheduleModel != null); Assertions.assertTrue(interestScheduleModel.loanProductRelatedDetail() != null); - Assertions.assertTrue(interestScheduleModel.mc() != null); Assertions.assertTrue(interestScheduleModel.installmentAmountInMultiplesOf() == null); - Assertions.assertTrue(interestScheduleModel.repayments() != null); - Assertions.assertEquals(4, interestScheduleModel.repayments().size()); + Assertions.assertTrue(interestScheduleModel.repaymentPeriods() != null); + Assertions.assertEquals(4, interestScheduleModel.repaymentPeriods().size()); Assertions.assertEquals(121, interestScheduleModel.getLoanTermInDays()); } @Test public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -191,7 +195,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); @@ -200,13 +203,13 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 1, 17.13, 0.007901833333, 0.79, 16.34, 83.66); checkPeriod(interestSchedule, 1, 0, 17.13, 0.007901833333, 0.66, 16.47, 67.19); checkPeriod(interestSchedule, 2, 0, 17.13, 0.007901833333, 0.53, 16.60, 50.59); checkPeriod(interestSchedule, 3, 0, 17.13, 0.007901833333, 0.40, 16.73, 33.86); @@ -216,7 +219,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay @Test public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -229,7 +232,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); @@ -238,13 +240,13 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 1, 17.13, 0.007901833333, 0.79, 16.34, 83.66); checkPeriod(interestSchedule, 1, 0, 17.13, 0.007901833333, 0.66, 16.47, 67.19); checkPeriod(interestSchedule, 2, 0, 17.13, 0.007901833333, 0.53, 16.60, 50.59); checkPeriod(interestSchedule, 3, 0, 17.13, 0.007901833333, 0.40, 16.73, 33.86); @@ -252,9 +254,10 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0); } + @Disabled("till interest rate change got implemented") @Test public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0201_4per() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -267,7 +270,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final BigDecimal interestRate = new BigDecimal("7"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); @@ -278,18 +280,17 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); final BigDecimal interestRateNewValue = new BigDecimal("4"); final LocalDate interestChangeDate = LocalDate.of(2024, 2, 2); - final LocalDate interestEffectiveDate = interestChangeDate.minusDays(1); - emiCalculator.changeInterestRate(interestSchedule, interestEffectiveDate, interestRateNewValue); + emiCalculator.changeInterestRate(interestSchedule, interestChangeDate, interestRateNewValue); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 1, 0, 16.88, 0.003333333333, 0.28, 16.60, 66.97); checkPeriod(interestSchedule, 2, 0, 16.88, 0.003333333333, 0.22, 16.66, 50.31); checkPeriod(interestSchedule, 3, 0, 16.88, 0.003333333333, 0.17, 16.71, 33.60); @@ -297,9 +298,10 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay checkPeriod(interestSchedule, 5, 0, 16.89, 0.003333333333, 0.06, 16.83, 0.0); } + @Disabled("till interest rate change got implemented") @Test public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0215_4per() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -312,7 +314,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final BigDecimal interestRate = new BigDecimal("7"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); @@ -323,17 +324,15 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); final BigDecimal interestRateNewValue = new BigDecimal("4"); final LocalDate interestChangeDate = LocalDate.of(2024, 2, 15); - final LocalDate interestEffectiveDate = interestChangeDate.minusDays(1); - emiCalculator.changeInterestRate(interestSchedule, interestEffectiveDate, interestRateNewValue); + emiCalculator.changeInterestRate(interestSchedule, interestChangeDate, interestRateNewValue); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 1, 0, 16.90, 0.002614942529, 0.22, 0.37, 16.53, 67.04); checkPeriod(interestSchedule, 1, 1, 16.90, 0.001839080460, 0.15, 0.37, 16.53, 67.04); @@ -348,7 +347,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay */ @Test public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_add_balance_correction_on0215() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -361,7 +360,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final BigDecimal interestRate = new BigDecimal("7"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); @@ -372,102 +370,101 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); // schedule 1st period 1st day - ProgressiveLoanInterestRepaymentModel repaymentDetails = emiCalculator - .getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 1)).get(); - Assertions.assertEquals(100, toDouble(repaymentDetails.getOutstandingBalance().getAmount())); - Assertions.assertEquals(82.99, toDouble(repaymentDetails.getRemainingBalance().getAmount())); - Assertions.assertEquals(17.01, toDouble(repaymentDetails.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.0, toDouble(repaymentDetails.getInterestDue().getAmount())); + PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 1, 1)); + Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance().getAmount())); + Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest().getAmount())); // schedule 2nd period last day - repaymentDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)).get(); - Assertions.assertEquals(83.57, toDouble(repaymentDetails.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.52, toDouble(repaymentDetails.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.49, toDouble(repaymentDetails.getInterestDue().getAmount())); + payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); + Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance().getAmount())); + Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest().getAmount())); - // partially pay off a period with balance correction + // pay off a period with balance correction final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15); - final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(-83.57)); + final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(16.77)); // get remaining balance and dues for a date - final ProgressiveLoanInterestRepaymentModel repaymentDetails1st = emiCalculator - .getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate).get(); + final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, + op1stCorrectionDate); Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getInterestDue().getAmount())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest().getAmount())); - emiCalculator.addBalanceCorrection(interestSchedule, op1stCorrectionDate, op1stCorrectionAmount); + emiCalculator.payPrincipal(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, op1stCorrectionAmount); + emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, + Money.of(monetaryCurrency, BigDecimal.valueOf(0.24))); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); - checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 0.24, 16.77, 66.80); - checkPeriod(interestSchedule, 1, 1, 17.01, 0.003017241379, 0.0, 0.24, 16.77, 66.80); - checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.18); - checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.46); - checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 16.65); - checkPeriod(interestSchedule, 5, 0, 16.75, 0.005833333333, 0.10, 16.65, 0.0); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 16.77, 66.80); + checkPeriod(interestSchedule, 1, 1, 17.01, 0.003017241379, 0.20, 0.24, 16.77, 66.80); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 0.59, 16.42, 50.38); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.66); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 16.85); + checkPeriod(interestSchedule, 5, 0, 16.95, 0.005833333333, 0.10, 16.85, 0.0); // totally pay off another period with balance correction final LocalDate op2ndCorrectionPeriodDueDate = LocalDate.of(2024, 4, 1); final LocalDate op2ndCorrectionDate = LocalDate.of(2024, 3, 1); - final Money op2ndCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(-66.80)); + final Money op2ndCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(16.42)); // get remaining balance and dues for a date - final ProgressiveLoanInterestRepaymentModel repaymentDetails2st = emiCalculator - .getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate).get(); + final PayableDetails repaymentDetails2st = emiCalculator.getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate, + op2ndCorrectionDate); Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(17.01, toDouble(repaymentDetails2st.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.0, toDouble(repaymentDetails2st.getInterestDue().getAmount())); + Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest().getAmount())); - emiCalculator.addBalanceCorrection(interestSchedule, op2ndCorrectionDate, op2ndCorrectionAmount); + emiCalculator.payPrincipal(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, op2ndCorrectionAmount); + emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, + Money.of(monetaryCurrency, BigDecimal.valueOf(0.49))); - checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); - checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 0.24, 16.77, 66.80); - checkPeriod(interestSchedule, 1, 1, 17.01, 0.003017241379, 0.0, 0.24, 16.77, 66.80); - checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0, 17.01, 49.79); - checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.07); - checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.19, 16.82, 16.25); - checkPeriod(interestSchedule, 5, 0, 16.34, 0.005833333333, 0.09, 16.25, 0.0); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 16.77, 50.38); + checkPeriod(interestSchedule, 1, 1, 17.01, 0.003017241379, 0.20, 0.24, 16.77, 50.38); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.29, 0.49, 16.52, 50.28); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.56); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 16.75); + checkPeriod(interestSchedule, 5, 0, 16.85, 0.005833333333, 0.10, 16.75, 0.0); // check numbers on last period due date LocalDate periodDueDate = LocalDate.of(2024, 7, 1); LocalDate payDate = LocalDate.of(2024, 7, 1); - final ProgressiveLoanInterestRepaymentModel repaymentDetails3rd = emiCalculator - .getPayableDetails(interestSchedule, periodDueDate, payDate).get(); - Assertions.assertEquals(16.25, toDouble(repaymentDetails3rd.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.25, toDouble(repaymentDetails3rd.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.09, toDouble(repaymentDetails3rd.getInterestDue().getAmount())); + final PayableDetails repaymentDetails3rd = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate); + Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance().getAmount())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest().getAmount())); // check numbers after the last period due date periodDueDate = LocalDate.of(2024, 7, 1); payDate = LocalDate.of(2024, 7, 15); - final ProgressiveLoanInterestRepaymentModel repaymentDetails4th = emiCalculator - .getPayableDetails(interestSchedule, periodDueDate, payDate).get(); - Assertions.assertEquals(16.25, toDouble(repaymentDetails4th.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.25, toDouble(repaymentDetails4th.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.14, toDouble(repaymentDetails4th.getInterestDue().getAmount())); + final PayableDetails repaymentDetails4th = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate); + Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance().getAmount())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest().getAmount())); // balance update on the last period, check the right interest interval split emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 10), Money.of(monetaryCurrency, BigDecimal.ZERO)); - final var lastRepaymentPeriod = interestSchedule.repayments().get(interestSchedule.repayments().size() - 1); - Assertions.assertTrue(lastRepaymentPeriod.isLastPeriod()); + final RepaymentPeriod lastRepaymentPeriod = interestSchedule.repaymentPeriods().get(interestSchedule.repaymentPeriods().size() - 1); Assertions.assertEquals(2, lastRepaymentPeriod.getInterestPeriods().size()); - Assertions.assertEquals(LocalDate.of(2024, 6, 1), lastRepaymentPeriod.getInterestPeriods().getFirst().getFromDate()); - Assertions.assertEquals(LocalDate.of(2024, 6, 10), lastRepaymentPeriod.getInterestPeriods().getFirst().getDueDate()); - Assertions.assertEquals(LocalDate.of(2024, 6, 10), lastRepaymentPeriod.getInterestPeriods().getLast().getFromDate()); - Assertions.assertEquals(LocalDate.of(2024, 7, 1), lastRepaymentPeriod.getInterestPeriods().getLast().getDueDate()); + Assertions.assertEquals(LocalDate.of(2024, 6, 1), lastRepaymentPeriod.getInterestPeriods().get(0).getFromDate()); + Assertions.assertEquals(LocalDate.of(2024, 6, 10), lastRepaymentPeriod.getInterestPeriods().get(0).getDueDate()); + Assertions.assertEquals(LocalDate.of(2024, 6, 10), lastRepaymentPeriod.getInterestPeriods().get(1).getFromDate()); + Assertions.assertEquals(LocalDate.of(2024, 7, 1), lastRepaymentPeriod.getInterestPeriods().get(1).getDueDate()); } @Test public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_on0215() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -480,7 +477,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final BigDecimal interestRate = new BigDecimal("7"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); @@ -490,7 +487,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); @@ -498,98 +495,140 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay // partially pay off a period with balance correction final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15); - final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(-83.57)); + final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(15.0)); // get remaining balance and dues for a date - final ProgressiveLoanInterestRepaymentModel repaymentDetails1st = emiCalculator - .getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate).get(); + final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, + op1stCorrectionDate); Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getInterestDue().getAmount())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest().getAmount())); - ProgressiveLoanInterestRepaymentModel details = null; + PayableDetails details = null; // check getPayableDetails forcast - details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)).get(); + details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance().getAmount())); - Assertions.assertEquals(83.57, toDouble(details.getCorrectedOutstandingBalance().getAmount())); - Assertions.assertEquals(16.52, toDouble(details.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.49, toDouble(details.getInterestDue().getAmount())); + Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.49, toDouble(details.getPayableInterest().getAmount())); // apply balance change and check again - emiCalculator.addBalanceCorrection(interestSchedule, op1stCorrectionDate, op1stCorrectionAmount); - details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)).get(); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), op1stCorrectionDate, op1stCorrectionAmount); + details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance().getAmount())); - Assertions.assertEquals(0, toDouble(details.getCorrectedOutstandingBalance().getAmount())); - Assertions.assertEquals(16.77, toDouble(details.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.24, toDouble(details.getInterestDue().getAmount())); - - emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 3, 1), - Money.of(monetaryCurrency, BigDecimal.valueOf(-66.80))); - emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 1), - Money.of(monetaryCurrency, BigDecimal.valueOf(-49.79))); - emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 5, 1), - Money.of(monetaryCurrency, BigDecimal.valueOf(-32.78))); - emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 1), - Money.of(monetaryCurrency, BigDecimal.valueOf(-15.77))); - - details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1)).get(); - Assertions.assertEquals(15.77, toDouble(details.getOutstandingBalance().getAmount())); - Assertions.assertEquals(0, toDouble(details.getCorrectedOutstandingBalance().getAmount())); - Assertions.assertEquals(15.77, toDouble(details.getPrincipalDue().getAmount())); - Assertions.assertEquals(0.0, toDouble(details.getInterestDue().getAmount())); + Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.49, toDouble(details.getPayableInterest().getAmount())); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), + Money.of(monetaryCurrency, BigDecimal.valueOf(1.43))); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), + Money.of(monetaryCurrency, BigDecimal.valueOf(0.58))); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), + Money.of(monetaryCurrency, BigDecimal.valueOf(16.77))); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), + Money.of(monetaryCurrency, BigDecimal.valueOf(0.24))); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 2, 15), + Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 2, 15), + Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 15), + Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 15), + Money.of(monetaryCurrency, BigDecimal.valueOf(15.77))); // check periods in model - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); - checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 0.24, 16.77, 66.80); - checkPeriod(interestSchedule, 1, 1, 17.01, 0.003017241379, 0.0, 0.24, 16.77, 66.80); - checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0, 17.01, 49.79); - checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0, 17.01, 32.78); - checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0, 17.01, 15.77); - checkPeriod(interestSchedule, 5, 0, 15.77, 0.005833333333, 0, 15.77, 0.0); + + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 0.24, 16.77, 0.0); + checkPeriod(interestSchedule, 1, 1, 17.01, 0.003017241379, 0.0, 0.24, 16.77, 0.0); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.0, 0.0, 17.01, 0.0); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.0, 0.0, 17.01, 0.0); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.0, 0.0, 17.01, 0.0); + checkPeriod(interestSchedule, 5, 0, 15.77, 0.005833333333, 0.0, 0.0, 15.77, 0.0); } - // @Test - // public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month_reschedule() { - // final MathContext mc = MoneyHelper.getMathContext(); - // final List expectedRepaymentPeriods = new ArrayList<>(); - // - // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); - // expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 15))); - // expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 15), LocalDate.of(2024, 4, 15))); - // expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 15), LocalDate.of(2024, 5, 15))); - // expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 15), LocalDate.of(2024, 6, 15))); - // expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 15), LocalDate.of(2024, 7, 15))); - // - // final BigDecimal interestRate = new BigDecimal("9.4822"); - // final Integer installmentAmountInMultiplesOf = null; - // - // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); - // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); - // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); - // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); - // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); - // Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); - // - // final ProgressiveLoanInterestScheduleModel interestSchedule = - // emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - // loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - // - // final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); - // emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - // - // checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - // checkPeriod(interestSchedule, 0, 0, 17.18, 0.008053375342, 0.81, 16.37, 83.66); - // checkPeriod(interestSchedule, 1, 0, 17.18, 0.011170810959, 0.93, 16.25, 67.38); - // checkPeriod(interestSchedule, 2, 0, 17.18, 0.008053375342, 0.54, 16.64, 50.74); - // checkPeriod(interestSchedule, 3, 0, 17.18, 0.007793589041, 0.40, 16.78, 33.96); - // checkPeriod(interestSchedule, 4, 0, 17.18, 0.008053375342, 0.27, 16.91, 17.05); - // checkPeriod(interestSchedule, 5, 0, 17.18, 0.007793589041, 0.13, 17.05, 0.0); - // } + @Test + public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_on0115() { + + final List expectedRepaymentPeriods = new ArrayList<>(); + + expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); + expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1))); + expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1))); + expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); + expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); + expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = new BigDecimal("7"); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); + + threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, + loanProductRelatedDetail, installmentAmountInMultiplesOf); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); + + // get remaining balance and dues on due date + PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), + LocalDate.of(2024, 2, 1)); + Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest().getAmount())); + + // check numbers on payoff date + payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15)); + Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest().getAmount())); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(16.75)); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(0.26)); + + // check again numbers are zero + // payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), + // LocalDate.of(2024, 2, 1)); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); + + payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1)); + Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal().getAmount())); + Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest().getAmount())); + + // check periods in model + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.26, 16.75, 15.21); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.002634408602, 0.26, 16.75, 15.21); + checkPeriod(interestSchedule, 0, 2, 17.01, 0.003198924731, 0.05, 0.26, 16.75, 15.21); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.09, 0.0, 17.01, 15.21); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.09, 0.0, 17.01, 15.21); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.09, 0.0, 17.01, 15.21); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.09, 0.0, 17.01, 15.21); + checkPeriod(interestSchedule, 5, 0, 15.71, 0.005833333333, 0.09, 0.5, 15.21, 0.0); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 1, 15), toMoney(15.21)); + + // check periods in model + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.26, 16.75, 0.0); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.002634408602, 0.26, 16.75, 0.0); + checkPeriod(interestSchedule, 0, 2, 17.01, 0.003198924731, 0.0, 0.26, 16.75, 0.0); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.0, 0.0, 17.01, 0.0); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.0, 0.0, 17.01, 0.0); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.0, 0.0, 17.01, 0.0); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.0, 0.0, 17.01, 0.0); + checkPeriod(interestSchedule, 5, 0, 15.21, 0.005833333333, 0.0, 0.0, 15.21, 0.0); + } @Test public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -602,7 +641,6 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); @@ -611,13 +649,13 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 1, 17.13, 0.007901833333, 0.79, 16.34, 83.66); checkPeriod(interestSchedule, 1, 0, 17.13, 0.007901833333, 0.66, 16.47, 67.19); checkPeriod(interestSchedule, 2, 0, 17.13, 0.007901833333, 0.53, 16.60, 50.59); checkPeriod(interestSchedule, 3, 0, 17.13, 0.007901833333, 0.40, 16.73, 33.86); @@ -627,9 +665,9 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(200)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, 300); - checkPeriod(interestSchedule, 0, 0, 51.33, 0.001784284946, 0.18, 2.02, 49.31, 250.69); - checkPeriod(interestSchedule, 0, 1, 51.33, 0.006117548387, 1.84, 2.02, 49.31, 250.69); + checkPeriod(interestSchedule, 0, 0, 51.33, 0.0, 0.0, 2.02, 49.31, 250.69); + checkPeriod(interestSchedule, 0, 1, 51.33, 0.001784284946, 0.18, 2.02, 49.31, 250.69); + checkPeriod(interestSchedule, 0, 2, 51.33, 0.006117548387, 1.84, 2.02, 49.31, 250.69); checkPeriod(interestSchedule, 1, 0, 51.33, 0.007901833333, 1.98, 49.35, 201.34); checkPeriod(interestSchedule, 2, 0, 51.33, 0.007901833333, 1.59, 49.74, 151.60); checkPeriod(interestSchedule, 3, 0, 51.33, 0.007901833333, 1.20, 50.13, 101.47); @@ -639,7 +677,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da @Test public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth30_repayEvery1Month() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -652,7 +690,6 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); @@ -661,13 +698,13 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 1, 17.13, 0.007901833333, 0.79, 16.34, 83.66); checkPeriod(interestSchedule, 1, 0, 17.13, 0.007901833333, 0.66, 16.47, 67.19); checkPeriod(interestSchedule, 2, 0, 17.13, 0.007901833333, 0.53, 16.60, 50.59); checkPeriod(interestSchedule, 3, 0, 17.13, 0.007901833333, 0.40, 16.73, 33.86); @@ -677,8 +714,8 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 2, 15), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 1, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.13, 0.007901833333, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); + checkPeriod(interestSchedule, 0, 1, 17.13, 0.007901833333, 0.79, 16.34, 83.66); checkPeriod(interestSchedule, 1, 0, 37.53, 0.003814678161, 0.32, 1.07, 36.46, 147.20); checkPeriod(interestSchedule, 1, 1, 37.53, 0.004087155172, 0.75, 1.07, 36.46, 147.20); checkPeriod(interestSchedule, 2, 0, 37.53, 0.007901833333, 1.16, 36.37, 110.83); @@ -689,7 +726,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 @Test public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month_backdated_disbursement() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = new ArrayList<>(); expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); @@ -702,7 +739,6 @@ public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_da final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); @@ -711,51 +747,31 @@ public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_da Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); - emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); - - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.10, 0.001784284946, 0.00, 0.61, 16.49, 83.51); - checkPeriod(interestSchedule, 0, 1, 17.10, 0.006117548387, 0.61, 0.61, 16.49, 83.51); - checkPeriod(interestSchedule, 1, 0, 17.10, 0.007901833333, 0.66, 16.44, 67.07); - checkPeriod(interestSchedule, 2, 0, 17.10, 0.007901833333, 0.53, 16.57, 50.50); - checkPeriod(interestSchedule, 3, 0, 17.10, 0.007901833333, 0.40, 16.70, 33.80); - checkPeriod(interestSchedule, 4, 0, 17.10, 0.007901833333, 0.27, 16.83, 16.97); - checkPeriod(interestSchedule, 5, 0, 17.10, 0.007901833333, 0.13, 16.97, 0.0); - - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(50)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, 150); - checkPeriod(interestSchedule, 0, 0, 25.66, 0.001019591398, 0.00, 0.96, 24.70, 125.30); - checkPeriod(interestSchedule, 0, 1, 25.66, 0.000764693548, 0.04, 0.96, 24.70, 125.30); - checkPeriod(interestSchedule, 0, 2, 25.66, 0.006117548387, 0.92, 0.96, 24.70, 125.30); - checkPeriod(interestSchedule, 1, 0, 25.66, 0.007901833333, 0.99, 24.67, 100.63); - checkPeriod(interestSchedule, 2, 0, 25.66, 0.007901833333, 0.80, 24.86, 75.77); - checkPeriod(interestSchedule, 3, 0, 25.66, 0.007901833333, 0.60, 25.06, 50.71); - checkPeriod(interestSchedule, 4, 0, 25.66, 0.007901833333, 0.40, 25.26, 25.45); - checkPeriod(interestSchedule, 5, 0, 25.65, 0.007901833333, 0.20, 25.45, 0.0); + disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(50)); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); // add disbursement on same date disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(25)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, 175); - checkPeriod(interestSchedule, 0, 0, 29.93, 0.001019591398, 0.00, 1.11, 28.82, 146.18); - checkPeriod(interestSchedule, 0, 1, 29.93, 0.000764693548, 0.04, 1.11, 28.82, 146.18); - checkPeriod(interestSchedule, 0, 2, 29.93, 0.006117548387, 1.07, 1.11, 28.82, 146.18); - checkPeriod(interestSchedule, 1, 0, 29.93, 0.007901833333, 1.16, 28.77, 117.41); - checkPeriod(interestSchedule, 2, 0, 29.93, 0.007901833333, 0.93, 29.00, 88.41); - checkPeriod(interestSchedule, 3, 0, 29.93, 0.007901833333, 0.70, 29.23, 59.18); - checkPeriod(interestSchedule, 4, 0, 29.93, 0.007901833333, 0.47, 29.46, 29.72); - checkPeriod(interestSchedule, 5, 0, 29.95, 0.007901833333, 0.23, 29.72, 0.0); + checkPeriod(interestSchedule, 0, 0, 29.94, 0.001019591398, 0.00, 1.15, 28.79, 146.21); + checkPeriod(interestSchedule, 0, 1, 29.94, 0.000764693548, 0.08, 1.15, 28.79, 146.21); + checkPeriod(interestSchedule, 0, 2, 29.94, 0.006117548387, 1.07, 1.15, 28.79, 146.21); + checkPeriod(interestSchedule, 1, 0, 29.94, 0.007901833333, 1.16, 28.78, 117.43); + checkPeriod(interestSchedule, 2, 0, 29.94, 0.007901833333, 0.93, 29.01, 88.42); + checkPeriod(interestSchedule, 3, 0, 29.94, 0.007901833333, 0.70, 29.24, 59.18); + checkPeriod(interestSchedule, 4, 0, 29.94, 0.007901833333, 0.47, 29.47, 29.71); + checkPeriod(interestSchedule, 5, 0, 29.94, 0.007901833333, 0.23, 29.71, 0.0); } @Test public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)), repayment(2, LocalDate.of(2024, 1, 12), LocalDate.of(2024, 2, 12)), @@ -767,7 +783,6 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); @@ -776,13 +791,13 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 17.13, 0.008044857759, 0.80, 16.33, 83.67); + checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.80, 16.33, 83.67); + checkPeriod(interestSchedule, 0, 1, 17.13, 0.008044857759, 0.80, 16.33, 83.67); checkPeriod(interestSchedule, 1, 0, 17.13, 0.008031371585, 0.67, 16.46, 67.21); checkPeriod(interestSchedule, 2, 0, 17.13, 0.007513218579, 0.50, 16.63, 50.58); checkPeriod(interestSchedule, 3, 0, 17.13, 0.008031371585, 0.41, 16.72, 33.86); @@ -792,7 +807,7 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua @Test public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = List.of( repayment(2, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), repayment(3, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)), @@ -802,7 +817,6 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { final BigDecimal interestRate = new BigDecimal("0"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); @@ -811,12 +825,11 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(1000)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); checkPeriod(interestSchedule, 0, 0, 250.0, 0.0, 0.0, 250.0, 750.0); checkPeriod(interestSchedule, 1, 0, 250.0, 0.0, 0.0, 250.0, 500.0); checkPeriod(interestSchedule, 2, 0, 250.0, 0.0, 0.0, 250.0, 250.0); @@ -825,7 +838,7 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { @Test public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 8)), repayment(2, LocalDate.of(2024, 1, 8), LocalDate.of(2024, 1, 15)), @@ -837,7 +850,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); @@ -846,13 +858,13 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 16.77, 0.001823500000, 0.18, 16.59, 83.41); + checkPeriod(interestSchedule, 0, 0, 16.77, 0.0, 0.0, 0.18, 16.59, 83.41); + checkPeriod(interestSchedule, 0, 1, 16.77, 0.001823500000, 0.18, 16.59, 83.41); checkPeriod(interestSchedule, 1, 0, 16.77, 0.001823500000, 0.15, 16.62, 66.79); checkPeriod(interestSchedule, 2, 0, 16.77, 0.001823500000, 0.12, 16.65, 50.14); checkPeriod(interestSchedule, 3, 0, 16.77, 0.001823500000, 0.09, 16.68, 33.46); @@ -862,7 +874,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r @Test public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 15)), repayment(2, LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 29)), @@ -871,7 +883,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_364.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); @@ -880,20 +891,20 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 33.57, 0.003647000000, 0.36, 33.21, 66.79); + checkPeriod(interestSchedule, 0, 0, 33.57, 0.0, 0.0, 0.36, 33.21, 66.79); + checkPeriod(interestSchedule, 0, 1, 33.57, 0.003647000000, 0.36, 33.21, 66.79); checkPeriod(interestSchedule, 1, 0, 33.57, 0.003647000000, 0.24, 33.33, 33.46); checkPeriod(interestSchedule, 2, 0, 33.58, 0.003647000000, 0.12, 33.46, 0.0); } @Test public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() { - final MathContext mc = MoneyHelper.getMathContext(); + final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)), repayment(2, LocalDate.of(2024, 1, 16), LocalDate.of(2024, 1, 31)), @@ -905,7 +916,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMa final BigDecimal interestRate = new BigDecimal("9.4822"); final Integer installmentAmountInMultiplesOf = null; - Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue()); @@ -914,13 +924,13 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMa Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, - loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + loanProductRelatedDetail, installmentAmountInMultiplesOf); final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); - checkPeriod(interestSchedule, 0, 0, 16.90, 0.003950916667, 0.40, 16.50, 83.50); + checkPeriod(interestSchedule, 0, 0, 16.90, 0.0, 0.0, 0.40, 16.50, 83.50); + checkPeriod(interestSchedule, 0, 1, 16.90, 0.003950916667, 0.40, 16.50, 83.50); checkPeriod(interestSchedule, 1, 0, 16.90, 0.003950916667, 0.33, 16.57, 66.93); checkPeriod(interestSchedule, 2, 0, 16.90, 0.003950916667, 0.26, 16.64, 50.29); checkPeriod(interestSchedule, 3, 0, 16.90, 0.003950916667, 0.20, 16.70, 33.59); @@ -928,127 +938,6 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMa checkPeriod(interestSchedule, 5, 0, 16.89, 0.003950916667, 0.07, 16.82, 0.0); } - // - // @Test - // public void testEMICalculation_Principal1000_NoInterest_repayEvery1Month() { - // final MathContext mc = MoneyHelper.getMathContext(); - // - // final BigDecimal interestRate = BigDecimal.valueOf(0); - // final Money outstandingBalance = Money.of(monetaryCurrency, BigDecimal.valueOf(1000)); - // - // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); - // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); - // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); - // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); - // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); - // - // final List expectedRepaymentPeriods = new ArrayList<>(); - // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); - // expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1))); - // expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1))); - // expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); - // - // final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, - // loanProductRelatedDetail, - // expectedRepaymentPeriods, 1, 4, mc); - // - // // 250.00 - // Assertions.assertEquals(new BigDecimal("250.00"), result.getEqualMonthlyInstallmentValue().getAmount()); - // - // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1()); - // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1()); - // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1()); - // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1()); - // - // // no more period, no more interest - // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1()); - // - // result.reset(); - // // check reset - // Assertions.assertEquals(BigDecimal.ZERO, result.getNextRepaymentPeriodRateFactorMinus1()); - // } - // - // @Test - // public void testUnsupportedRepaymentEveryYear() { - // final MathContext mc = MoneyHelper.getMathContext(); - // final List expectedRepaymentPeriods = new ArrayList<>(); - // - // final BigDecimal interestRate = BigDecimal.valueOf(9.4822); - // final BigDecimal principal = BigDecimal.valueOf(100); - // final Money outstandingBalance = Money.of(monetaryCurrency, principal); - // - // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); - // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); - // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue()); - // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.YEARS); - // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); - // - // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16))); - // - // try { - // final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, - // loanProductRelatedDetail, - // expectedRepaymentPeriods, 1, 6, mc); - // Assertions.fail(); - // } catch (Exception e) { - // Assertions.assertInstanceOf(UnsupportedOperationException.class, e); - // } - // } - // - // @Test - // public void testUnsupportedRepaymentEveryWholeTerm() { - // final MathContext mc = MoneyHelper.getMathContext(); - // final List expectedRepaymentPeriods = new ArrayList<>(); - // - // final BigDecimal interestRate = BigDecimal.valueOf(9.4822); - // final BigDecimal principal = BigDecimal.valueOf(100); - // final Money outstandingBalance = Money.of(monetaryCurrency, principal); - // - // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); - // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); - // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue()); - // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WHOLE_TERM); - // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); - // - // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16))); - // - // try { - // final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, - // loanProductRelatedDetail, - // expectedRepaymentPeriods, 1, 6, mc); - // Assertions.fail(); - // } catch (Exception e) { - // Assertions.assertInstanceOf(UnsupportedOperationException.class, e); - // } - // } - // - // @Test - // public void testInvalidRepaymentEveryValue() { - // final MathContext mc = MoneyHelper.getMathContext(); - // final List expectedRepaymentPeriods = new ArrayList<>(); - // - // final BigDecimal interestRate = BigDecimal.valueOf(9.4822); - // final BigDecimal principal = BigDecimal.valueOf(100); - // final Money outstandingBalance = Money.of(monetaryCurrency, principal); - // - // Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); - // Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); - // Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.INVALID.getValue()); - // Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.INVALID); - // Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); - // - // expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16))); - // - // try { - // final EMICalculationResult result = emiCalculator.calculateEMIValueAndRateFactors(outstandingBalance, - // loanProductRelatedDetail, - // expectedRepaymentPeriods, 1, 6, mc); - // Assertions.fail(); - // } catch (Exception e) { - // Assertions.assertInstanceOf(UnsupportedOperationException.class, e); - // } - // } - private static LoanScheduleModelRepaymentPeriod repayment(int periodNumber, LocalDate fromDate, LocalDate dueDate) { final Money zeroAmount = Money.zero(monetaryCurrency); return LoanScheduleModelRepaymentPeriod.repayment(periodNumber, fromDate, dueDate, zeroAmount, zeroAmount, zeroAmount, zeroAmount, @@ -1066,39 +955,35 @@ private static LoanRepaymentScheduleInstallment createPeriod(int periodId, Local } private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx, - final int interestIdx, final double emiValue, final double rateFactorMinus1, final double interestDue, - final double principalDue, final double remaingBalance) { - checkPeriod(interestScheduleModel, repaymentIdx, interestIdx, emiValue, rateFactorMinus1, interestDue, interestDue, principalDue, + final int interestIdx, final double emiValue, final double rateFactor, final double interestDue, final double principalDue, + final double remaingBalance) { + checkPeriod(interestScheduleModel, repaymentIdx, interestIdx, emiValue, rateFactor, interestDue, interestDue, principalDue, remaingBalance); } private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx, - final int interestIdx, final double emiValue, final double rateFactorMinus1, final double interestDue, + final int interestIdx, final double emiValue, final double rateFactor, final double interestDue, final double interestDueCumulated, final double principalDue, final double remaingBalance) { - final var repaymentPeriod = interestScheduleModel.repayments().get(repaymentIdx); - final var interestPeriod = repaymentPeriod.getInterestPeriods().get(interestIdx); - - Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEqualMonthlyInstallment().getAmount())); - Assertions.assertEquals(rateFactorMinus1, toDouble(interestPeriod.getRateFactorMinus1())); - Assertions.assertEquals(interestDue, toDouble(interestPeriod.getInterestDue().getAmount())); - Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getInterestDue().getAmount())); - Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getPrincipalDue().getAmount())); - Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getRemainingBalance().getAmount())); + final RepaymentPeriod repaymentPeriod = interestScheduleModel.repaymentPeriods().get(repaymentIdx); + final InterestPeriod interestPeriod = repaymentPeriod.getInterestPeriods().get(interestIdx); + + Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi().getAmount())); + Assertions.assertEquals(rateFactor, toDouble(applyMathContext(interestPeriod.getRateFactor()))); + Assertions.assertEquals(interestDue, toDouble(interestPeriod.getCalculatedDueInterest().getAmount())); + Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest().getAmount())); + Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal().getAmount())); + Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); } - private static void checkDisbursementOnPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx, - final Money disbursedAmount) { - checkDisbursementOnPeriod(interestScheduleModel, repaymentIdx, disbursedAmount.getAmount().doubleValue()); + private static double toDouble(final BigDecimal value) { + return value == null ? 0 : value.doubleValue(); } - private static void checkDisbursementOnPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx, - final double disbursedAmount) { - final var repaymentPeriod = interestScheduleModel.repayments().get(repaymentIdx); - Assertions.assertEquals(disbursedAmount, toDouble(repaymentPeriod.getDisbursedAmountInPeriod().getAmount())); + private static BigDecimal applyMathContext(final BigDecimal value) { + return value.setScale(MoneyHelper.getMathContext().getPrecision(), MoneyHelper.getRoundingMode()); } - private static double toDouble(final BigDecimal value) { - return value == null ? 0 : value.doubleValue(); + private static Money toMoney(final double value) { + return Money.of(monetaryCurrency, BigDecimal.valueOf(value)); } - }