From dfab66132baac1a013789a2cc3bda7d360153a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Soma=20S=C3=B6r=C3=B6s?= Date: Mon, 7 Jul 2025 14:33:42 +0200 Subject: [PATCH] FINERACT-1968: Progressive Loan schedule handling - interest handling (DECLINING BALANCE interest, Same as repayment period, Equal installments) --- .../calc/ProgressiveEMICalculatorTest.java | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) 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 ae11225e3..35a4ca6e2 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 @@ -104,6 +104,7 @@ class ProgressiveEMICalculatorTest { public void setupTestDefaults() { Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()).thenReturn(null); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE); Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()).thenReturn(InterestCalculationPeriodMethod.DAILY); Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true); @@ -3767,6 +3768,191 @@ class ProgressiveEMICalculatorTest { } } + @Nested + public class DecliningBalanceSameAsRepaymentPeriod { + + @BeforeEach + void beforeEach() { + Mockito.when(loanProductRelatedDetail.getInterestMethod()).thenReturn(InterestMethod.DECLINING_BALANCE); + Mockito.when(loanProductRelatedDetail.getInterestCalculationPeriodMethod()) + .thenReturn(InterestCalculationPeriodMethod.SAME_AS_REPAYMENT_PERIOD); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + } + + @Test + void test_3_of_1_month_period_multi_disbursement__exact_days() { + + final BigDecimal interestRate = BigDecimal.valueOf(12.0); + final LocalDate disbursementDate = LocalDate.of(2024, 6, 1); + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + + Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true); + + final List expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate); + final Integer installmentAmountInMultiplesOf = null; + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(500.0)); + checkPeriod(interestSchedule, 0, 170.01, 5.00, 165.01, 334.99, false); // + checkPeriod(interestSchedule, 1, 170.01, 3.35, 166.66, 168.33, false); // + checkPeriod(interestSchedule, 2, 170.01, 1.68, 168.33, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(500.0)); + + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); // + checkPeriod(interestSchedule, 1, 340.02, 6.70, 333.32, 336.66, false); // + checkPeriod(interestSchedule, 2, 340.03, 3.37, 336.66, 0.00, false); // + + final LocalDate disbursementDate2 = LocalDate.of(2024, 7, 15); + emiCalculator.addDisbursement(interestSchedule, disbursementDate2, toMoney(250.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); // + checkPeriod(interestSchedule, 1, 466.33, 8.07, 458.26, 461.72, false); // + checkPeriod(interestSchedule, 2, 466.34, 4.62, 461.72, 0.00, false); // + } + + @Test + void test_3_of_1_month_period_multi_disbursement__exact_days__on_period_due_date() { + + final BigDecimal interestRate = BigDecimal.valueOf(12.0); + final LocalDate disbursementDate = LocalDate.of(2024, 6, 1); + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + + Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(true); + + final List expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate); + final Integer installmentAmountInMultiplesOf = null; + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(500.0)); + checkPeriod(interestSchedule, 0, 170.01, 5.00, 165.01, 334.99, false); // + checkPeriod(interestSchedule, 1, 170.01, 3.35, 166.66, 168.33, false); // + checkPeriod(interestSchedule, 2, 170.01, 1.68, 168.33, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(500.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); // + checkPeriod(interestSchedule, 1, 340.02, 6.70, 333.32, 336.66, false); // + checkPeriod(interestSchedule, 2, 340.03, 3.37, 336.66, 0.00, false); // + + final LocalDate disbursementDate2 = LocalDate.of(2024, 7, 1); + emiCalculator.addDisbursement(interestSchedule, disbursementDate2, toMoney(250.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 919.98, false); // + checkPeriod(interestSchedule, 1, 466.90, 9.20, 457.70, 462.28, false); // + checkPeriod(interestSchedule, 2, 466.90, 4.62, 462.28, 0.00, false); // + } + + @Test + void test_3_of_1_month_period_multi_disbursement__NOT_exact_days__different_days() { + + final BigDecimal interestRate = BigDecimal.valueOf(12.0); + final LocalDate disbursementDate = LocalDate.of(2024, 6, 1); + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + + Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false); + + final List expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate); + final Integer installmentAmountInMultiplesOf = null; + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(500.0)); + checkPeriod(interestSchedule, 0, 170.01, 5.00, 165.01, 334.99, false); // + checkPeriod(interestSchedule, 1, 170.01, 3.35, 166.66, 168.33, false); // + checkPeriod(interestSchedule, 2, 170.01, 1.68, 168.33, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(500.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); // + checkPeriod(interestSchedule, 1, 340.02, 6.70, 333.32, 336.66, false); // + checkPeriod(interestSchedule, 2, 340.03, 3.37, 336.66, 0.00, false); // + + final LocalDate disbursementDate2 = LocalDate.of(2024, 7, 1); + final LocalDate disbursementDate3 = LocalDate.of(2024, 7, 15); + final LocalDate disbursementDate4 = LocalDate.of(2024, 7, 23); + emiCalculator.addDisbursement(interestSchedule, disbursementDate2, toMoney(50.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 719.98, false); // + checkPeriod(interestSchedule, 1, 365.40, 7.20, 358.20, 361.78, false); // + checkPeriod(interestSchedule, 2, 365.40, 3.62, 361.78, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate3, toMoney(50.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 769.98, false); // + checkPeriod(interestSchedule, 1, 390.77, 7.70, 383.07, 386.91, false); // + checkPeriod(interestSchedule, 2, 390.78, 3.87, 386.91, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate3, toMoney(50.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 819.98, false); // + checkPeriod(interestSchedule, 1, 416.15, 8.20, 407.95, 412.03, false); // + checkPeriod(interestSchedule, 2, 416.15, 4.12, 412.03, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate4, toMoney(50.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 869.98, false); // + checkPeriod(interestSchedule, 1, 441.53, 8.70, 432.83, 437.15, false); // + checkPeriod(interestSchedule, 2, 441.52, 4.37, 437.15, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate4, toMoney(50.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 919.98, false); // + checkPeriod(interestSchedule, 1, 466.90, 9.20, 457.70, 462.28, false); // + checkPeriod(interestSchedule, 2, 466.90, 4.62, 462.28, 0.00, false); // + } + + @Test + void test_3_of_1_month_period_multi_disbursement__NOT_exact_days() { + + final BigDecimal interestRate = BigDecimal.valueOf(12.0); + final LocalDate disbursementDate = LocalDate.of(2024, 6, 1); + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getNumberOfRepayments()).thenReturn(3); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + + Mockito.when(loanProductRelatedDetail.isAllowPartialPeriodInterestCalculation()).thenReturn(false); + + final List expectedRepaymentPeriods = generateExpectedRepaymentPeriods(disbursementDate); + final Integer installmentAmountInMultiplesOf = null; + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(500.0)); + checkPeriod(interestSchedule, 0, 170.01, 5.00, 165.01, 334.99, false); // + checkPeriod(interestSchedule, 1, 170.01, 3.35, 166.66, 168.33, false); // + checkPeriod(interestSchedule, 2, 170.01, 1.68, 168.33, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate, toMoney(500.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 669.98, false); // + checkPeriod(interestSchedule, 1, 340.02, 6.70, 333.32, 336.66, false); // + checkPeriod(interestSchedule, 2, 340.03, 3.37, 336.66, 0.00, false); // + + final LocalDate disbursementDate2 = LocalDate.of(2024, 7, 15); + emiCalculator.addDisbursement(interestSchedule, disbursementDate2, toMoney(130.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 799.98, false); // + checkPeriod(interestSchedule, 1, 406.00, 8.00, 398.00, 401.98, false); // + checkPeriod(interestSchedule, 2, 406.00, 4.02, 401.98, 0.00, false); // + + emiCalculator.addDisbursement(interestSchedule, disbursementDate2, toMoney(120.0)); + checkPeriod(interestSchedule, 0, 340.02, 10.00, 330.02, 919.98, false); // + checkPeriod(interestSchedule, 1, 466.90, 9.20, 457.70, 462.28, false); // + checkPeriod(interestSchedule, 2, 466.90, 4.62, 462.28, 0.00, false); // + } + } + // utilities private List generateExpectedRepaymentPeriods(LocalDate disbursementDate) { return switch (loanProductRelatedDetail.getRepaymentPeriodFrequencyType()) {