From 0e08903811496666108dcc840fe50562c96dd334 Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Thu, 5 Sep 2024 21:30:12 +0200 Subject: [PATCH] FINERACT-2081: Fix backdated multi disbursement on Progressive Loan --- .../ProgressiveLoanScheduleGenerator.java | 14 +- .../calc/ProgressiveEMICalculatorTest.java | 65 +++++++++ ...ntAllocationLoanRepaymentScheduleTest.java | 126 +++++++++++++++++- 3 files changed, 199 insertions(+), 6 deletions(-) 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 adfd1f1b7..5fd7d21c5 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 @@ -26,6 +26,7 @@ import java.math.RoundingMode; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -103,11 +104,15 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms); + final ArrayList disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas()); + disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate)); + for (LoanScheduleModelRepaymentPeriod repaymentPeriod : expectedRepaymentPeriods) { scheduleParams.setPeriodStartDate(repaymentPeriod.getFromDate()); scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate()); - processDisbursements(loanApplicationTerms, scheduleParams, interestScheduleModel, periods, chargesDueAtTimeOfDisbursement); + processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods, + chargesDueAtTimeOfDisbursement); repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber()); for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) { @@ -142,7 +147,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { } if (loanApplicationTerms.isMultiDisburseLoan()) { - processDisbursements(loanApplicationTerms, scheduleParams, null, periods, chargesDueAtTimeOfDisbursement); + processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, null, periods, chargesDueAtTimeOfDisbursement); } // determine fees and penalties for charges which depends on total @@ -169,11 +174,12 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { } } - private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final LoanScheduleParams scheduleParams, + private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, + final ArrayList disbursementDataList, final LoanScheduleParams scheduleParams, final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List periods, final BigDecimal chargesDueAtTimeOfDisbursement) { - for (DisbursementData disbursementData : loanApplicationTerms.getDisbursementDatas()) { + for (DisbursementData disbursementData : disbursementDataList) { final LocalDate disbursementDate = disbursementData.disbursementDate(); final LocalDate periodFromDate = scheduleParams.getPeriodStartDate(); final LocalDate periodDueDate = scheduleParams.getActualRepaymentDate(); 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 35762a2cd..753baf508 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 @@ -674,6 +674,71 @@ class ProgressiveEMICalculatorTest { checkPeriod(interestSchedule, 5, 0, 37.53, 0.007901833333, 0.29, 37.24, 0.0); } + @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))); + 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("9.4822"); + final Integer installmentAmountInMultiplesOf = null; + + 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); + Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, + loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + + 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); + + // 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); + } + @Test public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() { final MathContext mc = MoneyHelper.getMathContext(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 4c83df782..f0dc525d2 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -48,6 +48,7 @@ import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.LoanProduct; import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest; @@ -4927,7 +4928,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan // UC14-: Advanced payment allocation with Interest, originally calculated EMI, need adjustment, adjusted EMI is not // better than original, use original EMI and apply with multi disbursement // ADVANCED_PAYMENT_ALLOCATION_STRATEGY - // 1. Create a Loan product with Adv. Payment. Alloc. and with 5% Interest, 360/30, 1 repayment per month + // 1. Create a Loan product with Adv. Payment. Alloc. and with 9.4822% Interest, 360/30, 1 repayment per month // 2. Submit Loan and approve // 3. Disburse // 4. Validate Repayment Schedule @@ -5009,7 +5010,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan // UC14-: Advanced payment allocation with Interest, originally calculated EMI, need adjustment, adjusted EMI is not // better than original, use original EMI and apply with multi disbursement in two different periods // ADVANCED_PAYMENT_ALLOCATION_STRATEGY - // 1. Create a Loan product with Adv. Payment. Alloc. and with 5% Interest, 360/30, 1 repayment per month + // 1. Create a Loan product with Adv. Payment. Alloc. and with 9.4822% Interest, 360/30, 1 repayment per month // 2. Submit Loan and approve // 3. Disburse // 4. Validate Repayment Schedule @@ -5096,6 +5097,127 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan }); } + // UC14-: Advanced payment allocation with Interest, originally calculated EMI, need adjustment, adjusted EMI is not + // better than original, use original EMI and apply with multi disbursement, backdated 2nd disbursement, + // 3rd disbursement is same date as the 1st was. + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Payment. Alloc. and with 9.4822% Interest, 360/30, 1 repayment per month + // 2. Submit Loan and approve + // 3. 1st Disburse 8 of January + // 4. Validate Repayment Schedule + // 5. 2nd Disburse 5 of January + // 6. Validate Repayment Schedule + // 7. 3rd Disburse 8 of January (same day as 1st disbursement) + // 8. Validate Repayment Schedule + @Test + public void uc147c() { + final String operationDate = "8 January 2024"; + runAt(operationDate, () -> { + BigDecimal interestRatePerPeriod = BigDecimal.valueOf(9.4822); + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()// + .interestRatePerPeriod(interestRatePerPeriod.doubleValue())// + .interestCalculationPeriodType(RepaymentFrequencyType.DAYS)// + .interestRateFrequencyType(YEARS)// + .daysInMonthType(DaysInMonthType.DAYS_30.getValue())// + .daysInYearType(DaysInYearType.DAYS_360.getValue())// + .numberOfRepayments(6)// + .repaymentEvery(1)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())// + .repaymentStartDateType(LoanProduct.RepaymentStartDateTypeEnum.SUBMITTED_ON_DATE.ordinal())// + .enableDownPayment(false)// + .allowPartialPeriodInterestCalcualtion(false)// + .enableAutoRepaymentForDownPayment(null)// + .multiDisburseLoan(true)// + .disallowExpectedDisbursements(true)// + .allowApprovedDisbursedAmountsOverApplied(true)// + .overAppliedCalculationType("flat")// + .overAppliedNumber(10000)// + .maxTrancheCount(10)// + .outstandingLoanBalance(10000.0)// + .installmentAmountInMultiplesOf(null)// + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "1 January 2024", 100.0, + 6); + + applicationRequest = applicationRequest.numberOfRepayments(6)// + .loanTermFrequency(6)// + .loanTermFrequencyType(2)// + .interestRatePerPeriod(interestRatePerPeriod).interestCalculationPeriodType(DAYS)// + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)// + .repaymentEvery(1)// + .repaymentFrequencyType(2)// + .maxOutstandingLoanBalance(BigDecimal.valueOf(10000.0))// + ;// + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(100)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("1 January 2024").locale("en")); + + final String disbursementDate1st = "8 January 2024"; + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), // + new PostLoansLoanIdRequest()// + .actualDisbursementDate(disbursementDate1st).dateFormat(DATETIME_PATTERN)// + .transactionAmount(BigDecimal.valueOf(100.0)).locale("en"));// + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 102.6, 0.0, 100.0, 0.0, null); + + final String disbursementDate2nd = "5 January 2024"; + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), // + new PostLoansLoanIdRequest()// + .actualDisbursementDate(disbursementDate2nd).dateFormat(DATETIME_PATTERN)// + .transactionAmount(BigDecimal.valueOf(50.0)).locale("en"));// + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 153.95, 0.0, 150.0, 0.0, null); + + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 24.70, 0.0, 24.70, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.96, 0.0, + 0.96, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 24.67, 0.0, 24.67, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.99, 0.0, + 0.99, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 24.86, 0.0, 24.86, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.80, 0.0, + 0.80, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 25.06, 0.0, 25.06, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.60, 0.0, + 0.60, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 25.26, 0.0, 25.26, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.40, 0.0, + 0.40, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 25.45, 0.0, 25.45, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.20, 0.0, + 0.20, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(loanDetails.getNumberOfRepayments(), 6); + + // 2nd disbursement on the same date + final String disbursementDate3rd = "8 January 2024"; + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), // + new PostLoansLoanIdRequest()// + .actualDisbursementDate(disbursementDate3rd).dateFormat(DATETIME_PATTERN)// + .transactionAmount(BigDecimal.valueOf(25.0)).locale("en"));// + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 179.60, 0.0, 175.0, 0.0, null); + + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 28.82, 0.0, 28.82, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.11, 0.0, + 1.11, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 28.77, 0.0, 28.77, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.16, 0.0, + 1.16, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 29.00, 0.0, 29.00, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.93, 0.0, + 0.93, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 29.23, 0.0, 29.23, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.70, 0.0, + 0.70, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 29.46, 0.0, 29.46, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.47, 0.0, + 0.47, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 29.72, 0.0, 29.72, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.23, 0.0, + 0.23, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(loanDetails.getNumberOfRepayments(), 6); + }); + } + // uc148a: Advanced payment allocation, with Interest Recalculation in Loan Product and Adjust last, unpaid period // ADVANCED_PAYMENT_ALLOCATION_STRATEGY // 1. Create a Loan product with Adv. Pment. Alloc. with Interest Recalculation enabled and Adjust last, unpaid