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 4e0383c31af..521cbc9d8b6 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 @@ -307,7 +307,13 @@ private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestSchedu Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc); Optional findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) .reduce((first, second) -> second); - findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc))); + findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> { + repaymentPeriod.setEmi(repaymentPeriod.getEmi().add(diff, mc)); + if (repaymentPeriod.getEmi().isLessThanZero()) { + repaymentPeriod.setEmi(repaymentPeriod.getEmi().zero()); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + } + }); } private void calculateOutstandingBalance(ProgressiveLoanInterestScheduleModel scheduleModel) { 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 c6c5589be55..2f33c5efff6 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 @@ -1132,6 +1132,64 @@ public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEver checkPeriod(interestSchedule, 5, 0, 16.89, 0.003950916667, 0.07, 16.82, 0.0); } + @Test + public void soma_test_disbursedAmt1000_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() { + + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), + repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)), + repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)), + repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)), + repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)), + repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1)), + repayment(7, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 8, 1)), + repayment(8, LocalDate.of(2024, 8, 1), LocalDate.of(2024, 9, 1)), + repayment(9, LocalDate.of(2024, 9, 1), LocalDate.of(2024, 10, 1)), + repayment(10, LocalDate.of(2024, 10, 1), LocalDate.of(2024, 11, 1)), + repayment(11, LocalDate.of(2024, 11, 1), LocalDate.of(2024, 12, 1)), + repayment(12, LocalDate.of(2024, 12, 1), LocalDate.of(2025, 1, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(25); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).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.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(1000.0); + LocalDate disbursementDate = LocalDate.of(2024, 1, 1); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, disbursedAmount); + + for (int i = 0; i < 12; i++) { + LocalDate dueDate = expectedRepaymentPeriods.get(i).getDueDate(); + PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, dueDate, disbursementDate); + Money duePrincipal = dueAmounts.getDuePrincipal(); + if (duePrincipal.isGreaterThanZero()) { + emiCalculator.payPrincipal(interestSchedule, dueDate, disbursementDate, duePrincipal); + } + } + + checkPeriod(interestSchedule, 0, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 1, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 2, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 3, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 4, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 5, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 6, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 7, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 8, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 9, 95.02, 0.0, 95.02, 0.0, true); + checkPeriod(interestSchedule, 10, 49.8, 0.0, 49.8, 0.0, true); + checkPeriod(interestSchedule, 11, 0.0, 0.0, 0.0, 0.0, true); + } + @Test public void test_dailyInterest_disbursedAmt1000_dayInYears360_daysInMonth30_repayIn1Month() { @@ -1298,6 +1356,18 @@ private static void checkPeriod(final ProgressiveLoanInterestScheduleModel inter remaingBalance); } + private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx, + final double emiValue, final double interestDueCumulated, final double principalDue, final double remainingBalance, + final boolean fullyRepaid) { + final RepaymentPeriod repaymentPeriod = interestScheduleModel.repaymentPeriods().get(repaymentIdx); + + Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi())); + Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest())); + Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal())); + Assertions.assertEquals(remainingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance())); + Assertions.assertEquals(fullyRepaid, repaymentPeriod.isFullyPaid()); + } + private static void checkPeriod(final ProgressiveLoanInterestScheduleModel interestScheduleModel, final int repaymentIdx, final int interestIdx, final double emiValue, final double rateFactor, final double interestDue, final double interestDueCumulated, final double principalDue, final double remaingBalance) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 845c98a977c..a846f9bb356 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -1075,6 +1075,10 @@ protected Installment installment(double principalAmount, double interestAmount, return new Installment(principalAmount, interestAmount, null, null, totalOutstandingAmount, completed, dueDate, null, null); } + protected Installment fullyRepaidInstallment(double principalAmount, double interestAmount, String dueDate) { + return new Installment(principalAmount, interestAmount, null, null, 0.0, true, dueDate, null, null); + } + protected Installment installment(double principalAmount, double interestAmount, double feeAmount, double totalOutstandingAmount, Boolean completed, String dueDate) { return new Installment(principalAmount, interestAmount, feeAmount, null, totalOutstandingAmount, completed, dueDate, null, null); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java index f4398502584..3c68589cba6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java @@ -218,6 +218,127 @@ public void verifyInterestRefundCreatedForMerchantIssuedRefund() { }); } + @Test + public void verifyInterestRefundCreatedForMerchantIssuedRefundDay22HighInterest12month() { + AtomicReference loanIdRef = new AtomicReference<>(); + runAt("1 January 2021", () -> { + PostLoanProductsResponse loanProduct = loanProductHelper + .createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) // + .recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) // + ); + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 26.0, + 12, null); + Assertions.assertNotNull(loanId); + loanIdRef.set(loanId); + disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021"); + }); + runAt("22 January 2021", () -> { + Long loanId = loanIdRef.get(); + PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper + .makeLoanRepayment("MerchantIssuedRefund", "22 January 2021", 1000F, loanId.intValue()); + Assertions.assertNotNull(postLoansLoanIdTransactionsResponse); + Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); + + verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // + transaction(14.96, "Accrual", "22 January 2021"), // + transaction(14.96, "Interest Refund", "22 January 2021"), // + transaction(1000.0, "Merchant Issued Refund", "22 January 2021") // + ); + verifyRepaymentSchedule(loanId, installment(1000.0, null, "01 January 2021"), // + fullyRepaidInstallment(80.52, 14.96, "01 February 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 March 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 April 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 May 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 June 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 July 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 August 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 September 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 October 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 November 2021"), // + fullyRepaidInstallment(60.16, 0.0, "01 December 2021"), // + fullyRepaidInstallment(0.0, 0.0, "01 January 2022") // + ); + }); + } + + @Test + public void verifyFullMerchantIssuedRefundOnDay0HighInterest12month() { + runAt("1 January 2021", () -> { + PostLoanProductsResponse loanProduct = loanProductHelper + .createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) // + .recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) // + ); + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 26.0, + 12, null); + Assertions.assertNotNull(loanId); + disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021"); + PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper + .makeLoanRepayment("MerchantIssuedRefund", "1 January 2021", 1000F, loanId.intValue()); + Assertions.assertNotNull(postLoansLoanIdTransactionsResponse); + Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); + + verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // + transaction(1000.0, "Merchant Issued Refund", "01 January 2021") // + ); + verifyRepaymentSchedule(loanId, installment(1000.0, null, "01 January 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 February 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 March 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 April 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 May 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 June 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 July 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 August 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 September 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 October 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 November 2021"), // + fullyRepaidInstallment(45.2, 0.0, "01 December 2021"), // + fullyRepaidInstallment(0.0, 0.0, "01 January 2022") // + ); + }); + } + + @Test + public void verifyRepaymentDay0HighInterest12month() { + runAt("1 January 2021", () -> { + PostLoanProductsResponse loanProduct = loanProductHelper + .createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) // + .daysInYearType(DaysInYearType.ACTUAL) // + .addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) // + .recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) // + ); + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 26.0, + 12, null); + Assertions.assertNotNull(loanId); + disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021"); + PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment("Repayment", + "1 January 2021", 1000F, loanId.intValue()); + Assertions.assertNotNull(postLoansLoanIdTransactionsResponse); + Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); + + verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // + transaction(1000.0, "Repayment", "01 January 2021") // + ); + verifyRepaymentSchedule(loanId, installment(1000.0, null, "01 January 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 February 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 March 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 April 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 May 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 June 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 July 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 August 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 September 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 October 2021"), // + fullyRepaidInstallment(95.48, 0.0, "01 November 2021"), // + fullyRepaidInstallment(45.2, 0.0, "01 December 2021"), // + fullyRepaidInstallment(0.0, 0.0, "01 January 2022") // + ); + }); + } + @Test public void verifyUC01() { AtomicReference loanIdRef = new AtomicReference<>();