From 7e893eac780ebaf19b14a5a3d826ecf633e83f32 Mon Sep 17 00:00:00 2001 From: Ruchi Dhamankar Date: Wed, 27 Sep 2023 21:02:39 +0530 Subject: [PATCH] FINERACT-1958-Loan-repayment-calculation --- .../organisation/monetary/domain/Money.java | 9 + .../domain/LoanApplicationTerms.java | 4 + .../domain/AbstractLoanScheduleGenerator.java | 19 +- .../LoanAccountRepaymentCalculationTest.java | 447 ++++++++++++++++++ 4 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountRepaymentCalculationTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java index 6f955882e13..b8e2fc0fe0c 100644 --- a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java +++ b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/Money.java @@ -120,6 +120,15 @@ public static double roundToMultiplesOf(final double existingVal, final Integer return amountScaled; } + public static BigDecimal roundToMultiplesOf(final BigDecimal existingVal, final Integer inMultiplesOf) { + BigDecimal amountScaled = existingVal; + BigDecimal inMultiplesOfValue = BigDecimal.valueOf(inMultiplesOf.intValue()); + if (inMultiplesOfValue.compareTo(BigDecimal.ZERO) > 0) { + amountScaled = existingVal.divide(inMultiplesOfValue, 0, RoundingMode.HALF_UP).multiply(inMultiplesOfValue); + } + return amountScaled; + } + public static double ceiling(final double n, final double s) { double c; 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 6a48d8d2d6c..4ea52809dec 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 @@ -1774,4 +1774,8 @@ public LocalDate getSubmittedOnDate() { public boolean isScheduleExtensionForDownPaymentDisabled() { return isScheduleExtensionForDownPaymentDisabled; } + + public Integer getInstallmentAmountInMultiplesOf() { + return installmentAmountInMultiplesOf; + } } 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 0290c404f68..d1633a7a4d6 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 @@ -141,6 +141,11 @@ private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTe if (loanApplicationTerms.isDownPaymentEnabled()) { downPaymentAmount = MathUtil.percentageOf(scheduleParams.getOutstandingBalance().getAmount(), loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19); + if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) { + downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, + loanApplicationTerms.getInstallmentAmountInMultiplesOf()); + } + } Money calculatedAmortizableAmount = loanApplicationTerms.getPrincipal().minus(downPaymentAmount); scheduleParams.setOutstandingBalance(calculatedAmortizableAmount); @@ -155,6 +160,9 @@ private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTe if (loanApplicationTerms.isDownPaymentEnabled()) { downPaymentAmt = MathUtil.percentageOf(disburseAmt, loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19); + if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) { + downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, loanApplicationTerms.getInstallmentAmountInMultiplesOf()); + } } BigDecimal remainingPrincipalAmt = disburseAmt.subtract(downPaymentAmt); scheduleParams.setPrincipalToBeScheduled(Money.of(currency, remainingPrincipalAmt)); @@ -2022,6 +2030,9 @@ private LoanScheduleModelDownPaymentPeriod createDownPaymentPeriod(LoanApplicati LoanScheduleParams scheduleParams, LocalDate date, BigDecimal periodBaseAmount) { BigDecimal downPaymentAmount = MathUtil.percentageOf(periodBaseAmount, loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19); + if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) { + downPaymentAmount = Money.roundToMultiplesOf(downPaymentAmount, loanApplicationTerms.getInstallmentAmountInMultiplesOf()); + } Money downPayment = Money.of(loanApplicationTerms.getCurrency(), downPaymentAmount); LoanScheduleModelDownPaymentPeriod installment = LoanScheduleModelDownPaymentPeriod .downPayment(scheduleParams.getInstalmentNumber(), date, downPayment, scheduleParams.getOutstandingBalance()); @@ -2238,8 +2249,12 @@ private LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final L : loanApplicationTerms.getSubmittedOnDate(); BigDecimal downPaymentAmount = BigDecimal.ZERO; if (loanApplicationTerms.isDownPaymentEnabled()) { - downPaymentAmount = MathUtil.percentageOf(loanScheduleParams.getOutstandingBalance().getAmount(), - loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19); + double downPaymentAmt = MathUtil.percentageOf(loanScheduleParams.getOutstandingBalance().getAmount(), + loanApplicationTerms.getDisbursedAmountPercentageForDownPayment(), 19).doubleValue(); + if (loanApplicationTerms.getInstallmentAmountInMultiplesOf() != null) { + downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, loanApplicationTerms.getInstallmentAmountInMultiplesOf()); + } + downPaymentAmount = BigDecimal.valueOf(downPaymentAmt); } Money calculatedAmortizableAmount = principalToBeScheduled.minus(downPaymentAmount); loanScheduleParams.setOutstandingBalance(calculatedAmortizableAmount); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountRepaymentCalculationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountRepaymentCalculationTest.java new file mode 100644 index 00000000000..cb0c4a53e79 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAccountRepaymentCalculationTest.java @@ -0,0 +1,447 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(LoanTestLifecycleExtension.class) +public class LoanAccountRepaymentCalculationTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private LoanTransactionHelper loanTransactionHelper; + private ClientHelper clientHelper; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); + } + + @Test + public void loanAccountWithEnableDownPaymentWithInstallmentsInMultipleOfNullRepaymentScheduleCalculationTest() { + try { + + // Set business date + LocalDate disbursementDate = LocalDate.of(2023, 3, 3); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // down-payment configuration + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + // Loan Product creation with down-payment configuration and installmentAmountInMultiplesOf as null + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithEnableDownPaymentAndMultipleDisbursements( + loanTransactionHelper, enableDownPayment, "25", enableAutoRepaymentForDownPayment, null); + + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + // create loan account with amount 1250 + + final Integer loanId = createLoanAccountMultipleRepaymentsDisbursement(clientId, getLoanProductsProductResponse.getId(), "1250", + loanExternalIdStr, LoanProductTestBuilder.DEFAULT_STRATEGY); + + // Retrieve Loan with loanId + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + // verify down-payment details for Loan + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + // verify loan schedule + assertNotNull(loanDetails.getRepaymentSchedule()); + + // first period [2023-03-03 to 2023-03-03] down payment installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(1), 312.5, 1, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 3, 3), false); + + // second period [2023-03-03 to 2023-04-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(2), 312.5, 2, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 4, 3), false); + + // third period [2023-04-03 to 2023-05-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(3), 312.5, 3, LocalDate.of(2023, 4, 3), + LocalDate.of(2023, 5, 3), false); + + // fourth period [2023-05-03 to 2023-06-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(4), 312.5, 4, LocalDate.of(2023, 5, 3), + LocalDate.of(2023, 6, 3), false); + + // disbursement + loanTransactionHelper.disburseLoanWithTransactionAmount("03 March 2023", loanId, "1250"); + + loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails.getRepaymentSchedule()); + + // first period [2023-03-03 to 2023-03-03] down payment installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(1), 312.5, 1, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 3, 3), false); + + // second period [2023-03-03 to 2023-04-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(2), 312.5, 2, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 4, 3), false); + + // third period [2023-04-03 to 2023-05-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(3), 312.5, 3, LocalDate.of(2023, 4, 3), + LocalDate.of(2023, 5, 3), false); + + // fourth period [2023-05-03 to 2023-06-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(4), 312.5, 4, LocalDate.of(2023, 5, 3), + LocalDate.of(2023, 6, 3), false); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + } + + @Test + public void loanAccountWithEnableDownPaymentWithInstallmentsInMultipleOfSetAsOneRepaymentScheduleCalculationTest() { + try { + + // Set business date + LocalDate disbursementDate = LocalDate.of(2023, 3, 3); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // down-payment configuration + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + // Loan Product creation with down-payment configuration and installmentAmountInMultiplesOf as 1 + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithEnableDownPaymentAndMultipleDisbursements( + loanTransactionHelper, enableDownPayment, "25", enableAutoRepaymentForDownPayment, "1"); + + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + // create loan account with amount 1250 + + final Integer loanId = createLoanAccountMultipleRepaymentsDisbursement(clientId, getLoanProductsProductResponse.getId(), "1250", + loanExternalIdStr, LoanProductTestBuilder.DEFAULT_STRATEGY); + + // Retrieve Loan with loanId + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + // verify down-payment details for Loan + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + // verify loan schedule + assertNotNull(loanDetails.getRepaymentSchedule()); + + // first period [2023-03-03 to 2023-03-03] down payment installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(1), 313.0, 1, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 3, 3), false); + + // second period [2023-03-03 to 2023-04-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(2), 312.0, 2, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 4, 3), false); + + // third period [2023-04-03 to 2023-05-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(3), 312.0, 3, LocalDate.of(2023, 4, 3), + LocalDate.of(2023, 5, 3), false); + + // fourth period [2023-05-03 to 2023-06-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(4), 313.0, 4, LocalDate.of(2023, 5, 3), + LocalDate.of(2023, 6, 3), false); + + // disbursement + loanTransactionHelper.disburseLoanWithTransactionAmount("03 March 2023", loanId, "1250"); + + loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails.getRepaymentSchedule()); + + // first period [2023-03-03 to 2023-03-03] down payment installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(1), 313.0, 1, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 3, 3), false); + + // second period [2023-03-03 to 2023-04-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(2), 312.0, 2, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 4, 3), false); + + // third period [2023-04-03 to 2023-05-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(3), 312.0, 3, LocalDate.of(2023, 4, 3), + LocalDate.of(2023, 5, 3), false); + + // fourth period [2023-05-03 to 2023-06-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(4), 313.0, 4, LocalDate.of(2023, 5, 3), + LocalDate.of(2023, 6, 3), false); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + } + + @Test + public void loanAccountWithDisableDownPaymentWithInstallmentsInMultipleOfNullRepaymentScheduleCalculationTest() { + try { + + // Set business date + LocalDate disbursementDate = LocalDate.of(2023, 3, 3); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // down-payment configuration + Boolean enableDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + // Loan Product creation with down-payment configuration and installmentAmountInMultiplesOf as null + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithEnableDownPaymentAndMultipleDisbursements( + loanTransactionHelper, enableDownPayment, null, false, null); + + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + + // create loan account with amount 1250 + + final Integer loanId = createLoanAccountMultipleRepaymentsDisbursement(clientId, getLoanProductsProductResponse.getId(), "1250", + loanExternalIdStr, LoanProductTestBuilder.DEFAULT_STRATEGY); + + // Retrieve Loan with loanId + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + // verify down-payment details for Loan + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + + // verify loan schedule + assertNotNull(loanDetails.getRepaymentSchedule()); + + // first period [2023-03-03 to 2023-04-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(1), 416.67, 1, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 4, 3), false); + + // second period [2023-04-03 to 2023-05-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(2), 416.67, 2, LocalDate.of(2023, 4, 3), + LocalDate.of(2023, 5, 3), false); + + // third period [2023-05-03 to 2023-06-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(3), 416.66, 3, LocalDate.of(2023, 5, 3), + LocalDate.of(2023, 6, 3), false); + + // disbursement + loanTransactionHelper.disburseLoanWithTransactionAmount("03 March 2023", loanId, "1250"); + + loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails.getRepaymentSchedule()); + + // first period [2023-03-03 to 2023-04-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(1), 416.67, 1, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 4, 3), false); + + // second period [2023-04-03 to 2023-05-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(2), 416.67, 2, LocalDate.of(2023, 4, 3), + LocalDate.of(2023, 5, 3), false); + + // third period [2023-05-03 to 2023-06-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(3), 416.66, 3, LocalDate.of(2023, 5, 3), + LocalDate.of(2023, 6, 3), false); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + } + + @Test + public void loanAccountWithDisableDownPaymentWithInstallmentsInMultipleOfSetAsOneRepaymentScheduleCalculationTest() { + try { + + // Set business date + LocalDate disbursementDate = LocalDate.of(2023, 3, 3); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + // down-payment configuration + Boolean enableDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + // Loan Product creation with down-payment configuration and installmentAmountInMultiplesOf as 1 + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProductWithEnableDownPaymentAndMultipleDisbursements( + loanTransactionHelper, enableDownPayment, null, false, "1"); + + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + + // create loan account with amount 1250 + + final Integer loanId = createLoanAccountMultipleRepaymentsDisbursement(clientId, getLoanProductsProductResponse.getId(), "1250", + loanExternalIdStr, LoanProductTestBuilder.DEFAULT_STRATEGY); + + // Retrieve Loan with loanId + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + // verify down-payment details for Loan + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + + // verify loan schedule + assertNotNull(loanDetails.getRepaymentSchedule()); + + // first period [2023-03-03 to 2023-04-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(1), 417.0, 1, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 4, 3), false); + + // second period [2023-04-03 to 2023-05-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(2), 417.0, 2, LocalDate.of(2023, 4, 3), + LocalDate.of(2023, 5, 3), false); + + // third period [2023-05-03 to 2023-06-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(3), 416.0, 3, LocalDate.of(2023, 5, 3), + LocalDate.of(2023, 6, 3), false); + + // disbursement + loanTransactionHelper.disburseLoanWithTransactionAmount("03 March 2023", loanId, "1250"); + + loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails.getRepaymentSchedule()); + + // first period [2023-03-03 to 2023-04-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(1), 417.0, 1, LocalDate.of(2023, 3, 3), + LocalDate.of(2023, 4, 3), false); + + // second period [2023-04-03 to 2023-05-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(2), 417.0, 2, LocalDate.of(2023, 4, 3), + LocalDate.of(2023, 5, 3), false); + + // third period [2023-05-03 to 2023-06-03] regular installment + verifyPeriodDetails(loanDetails.getRepaymentSchedule().getPeriods().get(3), 416.0, 3, LocalDate.of(2023, 5, 3), + LocalDate.of(2023, 6, 3), false); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + + } + + private void verifyPeriodDetails(GetLoansLoanIdRepaymentPeriod period, double expectedAmount, int expectedPeriodNumber, + LocalDate expectedPeriodFromDate, LocalDate expectedPeriodDueDate, boolean isComplete) { + assertEquals(expectedPeriodNumber, period.getPeriod()); + assertEquals(expectedPeriodFromDate, period.getFromDate()); + assertEquals(expectedPeriodDueDate, period.getDueDate()); + assertEquals(expectedAmount, period.getTotalInstallmentAmountForPeriod()); + assertEquals(isComplete, period.getComplete()); + } + + private Integer createLoanAccountMultipleRepaymentsDisbursement(final Integer clientID, final Long loanProductID, + final String principalAmount, final String externalId, final String repaymentStartegy) { + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency("3") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("3").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("0").withInterestTypeAsDecliningBalance() + .withAmortizationTypeAsEqualInstallments().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate("03 March 2023").withSubmittedOnDate("03 March 2023").withLoanType("individual") + .withExternalId(externalId).withRepaymentStrategy(repaymentStartegy) + .build(clientID.toString(), loanProductID.toString(), null); + + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan("03 March 2023", "1250", loanId, null); + return loanId; + } + + private GetLoanProductsProductIdResponse createLoanProductWithEnableDownPaymentAndMultipleDisbursements( + LoanTransactionHelper loanTransactionHelper, Boolean enableDownPayment, String disbursedAmountPercentageForDownPayment, + boolean enableAutoRepaymentForDownPayment, String installmentAmountInMultiplesOf) { + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth() + .withRepaymentAfterEvery("1").withNumberOfRepayments("3").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withInterestTypeAsDecliningBalance().withAmortizationTypeAsEqualInstallments() + .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withDaysInMonth("30").withDaysInYear("365") + .withMoratorium("0", "0").withMultiDisburse().withDisallowExpectedDisbursements(true) + .withEnableDownPayment(enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment) + .withInstallmentAmountInMultiplesOf(installmentAmountInMultiplesOf).build(null); + final Integer loanProductId = loanTransactionHelper.getLoanProductId(loanProductJSON); + return loanTransactionHelper.getLoanProduct(loanProductId); + } + +}