diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index e9d38c438d7..94d99025073 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -69,6 +69,7 @@ public final class LoanApplicationTerms { private final Integer repaymentEvery; private final PeriodFrequencyType repaymentPeriodFrequencyType; + private long variationDays = 0L; private final Integer fixedLength; private final Integer nthDay; @@ -1845,16 +1846,16 @@ public LocalDate calculateMaxDateForFixedLength() { } switch (repaymentPeriodFrequencyType) { case DAYS: - maxDateForFixedLength = startDate.plusDays(fixedLength); + maxDateForFixedLength = startDate.plusDays(fixedLength + variationDays); break; case WEEKS: - maxDateForFixedLength = startDate.plusWeeks(fixedLength); + maxDateForFixedLength = startDate.plusWeeks(fixedLength + variationDays); break; case MONTHS: - maxDateForFixedLength = startDate.plusMonths(fixedLength); + maxDateForFixedLength = startDate.plusMonths(fixedLength + variationDays); break; case YEARS: - maxDateForFixedLength = startDate.plusYears(fixedLength); + maxDateForFixedLength = startDate.plusYears(fixedLength + variationDays); break; case INVALID: break; @@ -1871,4 +1872,12 @@ public LocalDate getRepaymentStartDate() { : getSubmittedOnDate(); } + public boolean isLastPeriod(final Integer periodNumber) { + return getNumberOfRepayments().equals(periodNumber); + } + + public void updateVariationDays(final long daysToAdd) { + this.variationDays += daysToAdd; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java index 213fa29d6d7..1f0380e520d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractProgressiveLoanScheduleGenerator.java @@ -91,8 +91,8 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer boolean isNextRepaymentAvailable = true; while (!scheduleParams.getOutstandingBalance().isZero()) { - scheduleParams.setActualRepaymentDate(getScheduledDateGenerator() - .generateNextRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment)); + scheduleParams.setActualRepaymentDate(getScheduledDateGenerator().generateNextRepaymentDate( + scheduleParams.getActualRepaymentDate(), loanApplicationTerms, isFirstRepayment, scheduleParams.getPeriodNumber())); AdjustedDateDetailsDTO adjustedDateDetailsDTO = getScheduledDateGenerator() .adjustRepaymentDate(scheduleParams.getActualRepaymentDate(), loanApplicationTerms, holidayDetailDTO); scheduleParams.setActualRepaymentDate(adjustedDateDetailsDTO.getChangedActualRepaymentDate()); @@ -601,6 +601,7 @@ private LoanTermVariationParams applyLoanTermVariations(final LoanApplicationTer LoanTermVariationsData loanTermVariationsData = loanApplicationTerms.getLoanTermVariations().nextDueDateVariation(); if (DateUtils.isEqual(modifiedScheduledDueDate, loanTermVariationsData.getTermVariationApplicableFrom())) { modifiedScheduledDueDate = loanTermVariationsData.getDateValue(); + loanApplicationTerms.updateVariationDays(DateUtils.getDifferenceInDays(scheduledDueDate, modifiedScheduledDueDate)); if (!loanTermVariationsData.isSpecificToInstallment()) { scheduleParams.setActualRepaymentDate(modifiedScheduledDueDate); loanApplicationTerms.setNewScheduledDueDateStart(modifiedScheduledDueDate); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java index fbf889e7a60..7d3685d7b27 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGenerator.java @@ -103,10 +103,20 @@ public LocalDate generateNextRepaymentDate(final LocalDate lastRepaymentDate, fi } } - final LocalDate maxDateForFixedLength = loanApplicationTerms.calculateMaxDateForFixedLength(); - // Fixed Length validation - if (maxDateForFixedLength != null && DateUtils.isAfter(dueRepaymentPeriodDate, maxDateForFixedLength)) { - dueRepaymentPeriodDate = maxDateForFixedLength; + return dueRepaymentPeriodDate; + } + + @Override + public LocalDate generateNextRepaymentDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms, + final boolean isFirstRepayment, final Integer periodNumber) { + LocalDate dueRepaymentPeriodDate = generateNextRepaymentDate(lastRepaymentDate, loanApplicationTerms, isFirstRepayment); + + // Fixed Length validation only for Last Installment + if (loanApplicationTerms.isLastPeriod(periodNumber)) { + final LocalDate maxDateForFixedLength = loanApplicationTerms.calculateMaxDateForFixedLength(); + if (maxDateForFixedLength != null && dueRepaymentPeriodDate.compareTo(maxDateForFixedLength) != 0) { + dueRepaymentPeriodDate = maxDateForFixedLength; + } } return dueRepaymentPeriodDate; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java index f2e6de21c10..50edf46fa41 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ScheduledDateGenerator.java @@ -34,6 +34,9 @@ LocalDate idealDisbursementDateBasedOnFirstRepaymentDate(PeriodFrequencyType rep LocalDate generateNextRepaymentDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms, boolean isFirstRepayment); + LocalDate generateNextRepaymentDate(LocalDate lastRepaymentDate, LoanApplicationTerms loanApplicationTerms, boolean isFirstRepayment, + Integer periodNumber); + AdjustedDateDetailsDTO adjustRepaymentDate(LocalDate dueRepaymentPeriodDate, LoanApplicationTerms loanApplicationTerms, HolidayDetailDTO holidayDetailDTO); 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 9cf4c679192..ecc47ee3990 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 @@ -3819,6 +3819,198 @@ public void uc132() { }); } + // UC133: Advanced payment allocation with higher Fixed Length for 50 days than Loan Term for 45 days (3 repayments + // every 15 days) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve + // 3. Disburse + // 4. Validate Repayment Schedule + @Test + public void uc133() { + final String operationDate = "22 November 2023"; + runAt(operationDate, () -> { + final Integer fixedLength = 50; // 50 days + final Integer repaymentFrequencyType = RepaymentFrequencyType.DAYS; + final Integer numberOfRepayments = 3; + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(numberOfRepayments).repaymentEvery(15).repaymentFrequencyType(repaymentFrequencyType.longValue()) + .fixedLength(fixedLength); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "22 November 2023", + 1000.0, numberOfRepayments) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("22 November 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("22 November 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(100.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + LOG.info("Loan {} {}", loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule().getPeriods() + .get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()); + assertEquals( + Utils.getDifferenceInDays(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule() + .getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()), + fixedLength.longValue()); + assertEquals(loanDetails.getNumberOfRepayments(), numberOfRepayments); + assertTrue(loanDetails.getStatus().getActive()); + }); + } + + // UC134: Advanced payment allocation with Fixed Length for 119 days and Loan Term for 120 days (4 repayments every + // 30 + // days) and Reschedule + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve + // 3. Disburse + // 4. Validate Repayment Schedule + // 5. ReSchedule + @Test + public void uc134() { + final String operationDate = "1 January 2024"; + runAt(operationDate, () -> { + final Integer fixedLength = 119; // 120 days + final Integer repaymentFrequencyType = RepaymentFrequencyType.DAYS; + final Integer numberOfRepayments = 4; + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(numberOfRepayments).repaymentEvery(30).fixedLength(fixedLength); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 1000.0, + numberOfRepayments); + + applicationRequest = applicationRequest.numberOfRepayments(numberOfRepayments).loanTermFrequency(120) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(30); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 31), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 3, 31), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 4, 29), 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(loanDetails.getNumberOfRepayments(), numberOfRepayments); + assertEquals( + Utils.getDifferenceInDays(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule() + .getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()), + fixedLength.longValue()); + + PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper + .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanDetails.getId()).locale("en") + .dateFormat(DATETIME_PATTERN).rescheduleReasonId(1L).rescheduleFromDate("1 March 2024") + .adjustedDueDate("15 March 2024").submittedOnDate("16 January 2024")); + + loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(), + new PostUpdateRescheduleLoansRequest().approvedOnDate("16 January 2024").locale("en").dateFormat(DATETIME_PATTERN)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 31), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 15), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 14), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 13), 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(loanDetails.getNumberOfRepayments(), numberOfRepayments); + assertEquals( + Utils.getDifferenceInDays(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule() + .getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()), + fixedLength.longValue() + 14); // Days in Reschedule + assertTrue(loanDetails.getStatus().getActive()); + }); + } + + // UC135: Advanced payment allocation with Fixed Length for 119 days and Loan Term for 120 days (4 repayments every + // 30 + // days) and Reschedule + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and No Interest + // 2. Submit Loan and approve + // 3. Disburse + // 4. Validate Repayment Schedule + // 5. ReSchedule Last installment + @Test + public void uc135() { + final String operationDate = "1 January 2024"; + runAt(operationDate, () -> { + final Integer fixedLength = 119; // 120 days + final Integer repaymentFrequencyType = RepaymentFrequencyType.DAYS; + final Integer numberOfRepayments = 4; + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .numberOfRepayments(numberOfRepayments).repaymentEvery(30).fixedLength(fixedLength); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 1000.0, + numberOfRepayments); + + applicationRequest = applicationRequest.numberOfRepayments(numberOfRepayments).loanTermFrequency(120) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(30); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 31), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 3, 31), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 4, 29), 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(loanDetails.getNumberOfRepayments(), numberOfRepayments); + assertEquals( + Utils.getDifferenceInDays(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule() + .getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()), + fixedLength.longValue()); + + PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper + .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanDetails.getId()).locale("en") + .dateFormat(DATETIME_PATTERN).rescheduleReasonId(1L).rescheduleFromDate("29 April 2024") + .adjustedDueDate("5 May 2024").submittedOnDate("16 January 2024")); + + loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(), + new PostUpdateRescheduleLoansRequest().approvedOnDate("16 January 2024").locale("en").dateFormat(DATETIME_PATTERN)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 1000.0, 0.0, 1000.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 1, 31), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 3, 31), 250.0, 0.0, 250.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 5), 250.0, 0.0, 250.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(loanDetails.getNumberOfRepayments(), numberOfRepayments); + assertEquals( + Utils.getDifferenceInDays(loanDetails.getTimeline().getActualDisbursementDate(), loanDetails.getRepaymentSchedule() + .getPeriods().get(loanDetails.getRepaymentSchedule().getPeriods().size() - 1).getDueDate()), + fixedLength.longValue() + 6); // Days in Reschedule + assertTrue(loanDetails.getStatus().getActive()); + }); + } + private static List getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) { AtomicInteger integer = new AtomicInteger(1); return Arrays.stream(paymentAllocationTypes).map(pat -> {