diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index e152c78da..a5edd7abc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -284,6 +284,15 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener // backup for pre-close transaction updateCompoundingDetails(scheduleParams, periodStartDateApplicableForInterest); + if (loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.isDownPaymentEnabled()) { + long numberOfDownPaymentPeriods = periods.stream() // + .filter(LoanScheduleModelPeriod::isDownPaymentPeriod).count(); + Long notApplicableForInstallmentRecalculationDownPaymentPeriods = numberOfDownPaymentPeriods - 1; + // Only the first down payment installment counts for the number of repayments + scheduleParams.setPeriodNumber( + scheduleParams.getPeriodNumber() - notApplicableForInstallmentRecalculationDownPaymentPeriods.intValue()); + } + // 5 determine principal,interest of repayment period PrincipalInterest principalInterestForThisPeriod = calculatePrincipalInterestComponentsForPeriod( this.paymentPeriodsInOneYearCalculator, currentPeriodParams.getInterestCalculationGraceOnRepaymentPeriodFraction(), diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 831631693..1ce71ca2b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -1221,7 +1221,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa periods.add(periodData); this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(data.getPrincipal()); disbursementPeriodIds.add(data.getId()); - } else if (data.isDueForDisbursement(fromDate, dueDate)) { + } else if (data.isDueForDisbursement(fromDate, dueDate) && !disbursementPeriodIds.contains(data.getId())) { if (!excludePastUndisbursed || data.isDisbursed() || !data.disbursementDate().isBefore(DateUtils.getBusinessLocalDate())) { principal = principal.add(data.getPrincipal()); @@ -1235,7 +1235,23 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa } periods.add(periodData); this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(data.getPrincipal()); + disbursementPeriodIds.add(data.getId()); } + } else if (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate) + && !disbursementPeriodIds.contains(data.getId())) { + principal = principal.add(data.getPrincipal()); + LoanSchedulePeriodData periodData = null; + if (data.getChargeAmount() == null) { + periodData = LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(), + disbursementChargeAmount, data.isDisbursed()); + } else { + periodData = LoanSchedulePeriodData.disbursementOnlyPeriod(data.disbursementDate(), data.getPrincipal(), + disbursementChargeAmount.add(data.getChargeAmount()).subtract(waivedChargeAmount), + data.isDisbursed()); + } + periods.add(periodData); + this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(data.getPrincipal()); + disbursementPeriodIds.add(data.getId()); } } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java index 1adda179d..99d0d7194 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java @@ -338,6 +338,7 @@ public class LoanRepaymentScheduleWithDownPaymentTest { assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedOutstandingLoanBalanceOnDisbursement = 1000.00; Double expectedDownPaymentAmount = 250.00; LocalDate expectedDownPaymentDueDate = LocalDate.of(2022, 9, 3); Double expectedRepaymentAmount = 250.00; @@ -348,21 +349,110 @@ public class LoanRepaymentScheduleWithDownPaymentTest { LocalDate expectedThirdRepaymentDueDate = LocalDate.of(2022, 12, 3); Double outstandingBalanceOnThirdRepayment = 0.00; - assertTrue(periods.stream() // - .anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) // - && expectedDownPaymentDueDate.equals(period.getDueDate()))); - assertTrue(periods.stream() - .anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod()) - && expectedFirstRepaymentDueDate.equals(period.getDueDate()) - && outstandingBalanceOnFirstRepayment.equals(period.getPrincipalLoanBalanceOutstanding()))); - assertTrue(periods.stream() - .anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod()) - && expectedSecondRepaymentDueDate.equals(period.getDueDate()) - && outstandingBalanceOnSecondRepayment.equals(period.getPrincipalLoanBalanceOutstanding()))); - assertTrue(periods.stream() - .anyMatch(period -> expectedRepaymentAmount.equals(period.getTotalDueForPeriod()) - && expectedThirdRepaymentDueDate.equals(period.getDueDate()) - && outstandingBalanceOnThirdRepayment.equals(period.getPrincipalLoanBalanceOutstanding()))); + GetLoansLoanIdRepaymentPeriod firstDisbursementPeriod = periods.get(0); + assertEquals(expectedDownPaymentDueDate, firstDisbursementPeriod.getDueDate()); + assertEquals(expectedOutstandingLoanBalanceOnDisbursement, firstDisbursementPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod firstDownPaymentPeriod = periods.get(1); + assertEquals(expectedDownPaymentAmount, firstDownPaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedDownPaymentDueDate, firstDownPaymentPeriod.getDueDate()); + + GetLoansLoanIdRepaymentPeriod firstRepaymentPeriod = periods.get(2); + assertEquals(expectedRepaymentAmount, firstRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedFirstRepaymentDueDate, firstRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnFirstRepayment, firstRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod secondRepaymentPeriod = periods.get(3); + assertEquals(expectedRepaymentAmount, secondRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedSecondRepaymentDueDate, secondRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnSecondRepayment, secondRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod thirdRepaymentPeriod = periods.get(4); + assertEquals(expectedRepaymentAmount, thirdRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedThirdRepaymentDueDate, thirdRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnThirdRepayment, thirdRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); + } + + @Test + public void loanRepaymentScheduleWithMultiDisbursementProductTwoDisbursementAndThreeRepaymentsAndDownPayment() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = true; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, true); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createApproveAndDisburseTwiceLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr, "4"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + loanTransactionHelper.printRepaymentSchedule(loanDetails); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedOutstandingLoanBalanceOnFirstDisbursement = 700.00; + Double expectedFirstDownPaymentAmount = 175.00; + LocalDate expectedFirstDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedOutstandingLoanBalanceOnSecondDisbursement = 300.00; + Double expectedSecondDownPaymentAmount = 75.00; + LocalDate expectedSecondDownPaymentDueDate = LocalDate.of(2022, 9, 4); + Double expectedRepaymentAmount = 250.00; + LocalDate expectedFirstRepaymentDueDate = LocalDate.of(2022, 10, 3); + Double outstandingBalanceOnFirstRepayment = 500.00; + LocalDate expectedSecondRepaymentDueDate = LocalDate.of(2022, 11, 3); + Double outstandingBalanceOnSecondRepayment = 250.00; + LocalDate expectedThirdRepaymentDueDate = LocalDate.of(2022, 12, 3); + Double outstandingBalanceOnThirdRepayment = 0.00; + + GetLoansLoanIdRepaymentPeriod firstDisbursementPeriod = periods.get(0); + assertEquals(expectedFirstDownPaymentDueDate, firstDisbursementPeriod.getDueDate()); + assertEquals(expectedOutstandingLoanBalanceOnFirstDisbursement, firstDisbursementPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod firstDownPaymentPeriod = periods.get(1); + assertEquals(expectedFirstDownPaymentAmount, firstDownPaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedFirstDownPaymentDueDate, firstDownPaymentPeriod.getDueDate()); + + GetLoansLoanIdRepaymentPeriod secondDisbursementPeriod = periods.get(2); + assertEquals(expectedSecondDownPaymentDueDate, secondDisbursementPeriod.getDueDate()); + assertEquals(expectedOutstandingLoanBalanceOnSecondDisbursement, secondDisbursementPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod secondDownPaymentPeriod = periods.get(3); + assertEquals(expectedSecondDownPaymentAmount, secondDownPaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedSecondDownPaymentDueDate, secondDownPaymentPeriod.getDueDate()); + + GetLoansLoanIdRepaymentPeriod firstRepaymentPeriod = periods.get(4); + assertEquals(expectedRepaymentAmount, firstRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedFirstRepaymentDueDate, firstRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnFirstRepayment, firstRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod secondRepaymentPeriod = periods.get(5); + assertEquals(expectedRepaymentAmount, secondRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedSecondRepaymentDueDate, secondRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnSecondRepayment, secondRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod thirdRepaymentPeriod = periods.get(6); + assertEquals(expectedRepaymentAmount, thirdRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedThirdRepaymentDueDate, thirdRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnThirdRepayment, thirdRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); } private Integer createLoanProductWithDownPaymentConfiguration(final LoanTransactionHelper loanTransactionHelper, @@ -405,7 +495,7 @@ public class LoanRepaymentScheduleWithDownPaymentTest { private Integer createApproveAndDisburseTwiceLoanAccount(final Integer clientID, final Long loanProductID, final String externalId, final String numberOfRepayments) { - String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1") + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency(numberOfRepayments) .withLoanTermFrequencyAsMonths().withNumberOfRepayments(numberOfRepayments).withRepaymentEveryAfter("1") .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance() .withAmortizationTypeAsEqualPrincipalPayments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod()