From af37e07f31a97154e1ccae2c678ba6ca8ef5c08f Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Thu, 23 Jan 2025 18:15:33 +0200 Subject: [PATCH] FINERACT-2152: Interest pause during one period and between two periods --- .../fineract/client/util/FineractClient.java | 3 + .../fineract/test/api/ApiConfiguration.java | 6 ++ .../loan/LoanInterestPauseStepDef.java | 53 +++++++++++ .../features/LoanInterestPause.feature | 74 +++++++++++++++ ...InterestPauseWritePlatformServiceImpl.java | 30 +++--- .../data/LoanTermVariationsDataWrapper.java | 33 +++---- .../domain/LoanTermVariationType.java | 4 + .../loanaccount/service/LoanAssembler.java | 7 ++ ...edPaymentScheduleTransactionProcessor.java | 18 +++- .../loanschedule/data/InterestPeriod.java | 24 ++--- .../ProgressiveLoanInterestScheduleModel.java | 92 ++++++++++++++++--- .../loanschedule/data/RepaymentPeriod.java | 2 +- .../ProgressiveLoanScheduleGenerator.java | 24 +++-- .../loanproduct/calc/EMICalculator.java | 2 + .../calc/ProgressiveEMICalculator.java | 14 +++ .../calc/ProgressiveEMICalculatorTest.java | 76 +++++++++++++++ .../service/LoanAssemblerImpl.java | 27 +++++- .../starter/LoanAccountConfiguration.java | 4 +- 18 files changed, 414 insertions(+), 79 deletions(-) create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanInterestPauseStepDef.java create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/LoanInterestPause.feature diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java index f722c3488..ec6aa161b 100644 --- a/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java +++ b/fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java @@ -88,6 +88,7 @@ import org.apache.fineract.client.services.LoanChargesApi; import org.apache.fineract.client.services.LoanCobCatchUpApi; import org.apache.fineract.client.services.LoanCollateralApi; import org.apache.fineract.client.services.LoanDisbursementDetailsApi; +import org.apache.fineract.client.services.LoanInterestPauseApi; import org.apache.fineract.client.services.LoanProductsApi; import org.apache.fineract.client.services.LoanReschedulingApi; import org.apache.fineract.client.services.LoanTransactionsApi; @@ -290,6 +291,7 @@ public final class FineractClient { public final UserGeneratedDocumentsApi templates; public final UsersApi users; public final WorkingDaysApi workingDays; + public final LoanInterestPauseApi loanInterestPauseApi; public final ExternalAssetOwnersApi externalAssetOwners; public final ExternalAssetOwnerLoanProductAttributesApi externalAssetOwnerLoanProductAttributes; @@ -415,6 +417,7 @@ public final class FineractClient { templates = retrofit.create(UserGeneratedDocumentsApi.class); users = retrofit.create(UsersApi.class); workingDays = retrofit.create(WorkingDaysApi.class); + loanInterestPauseApi = retrofit.create(LoanInterestPauseApi.class); } public static Builder builder() { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/api/ApiConfiguration.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/api/ApiConfiguration.java index 0607b76b9..8054d746a 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/api/ApiConfiguration.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/api/ApiConfiguration.java @@ -40,6 +40,7 @@ import org.apache.fineract.client.services.JournalEntriesApi; import org.apache.fineract.client.services.LoanAccountLockApi; import org.apache.fineract.client.services.LoanChargesApi; import org.apache.fineract.client.services.LoanCobCatchUpApi; +import org.apache.fineract.client.services.LoanInterestPauseApi; import org.apache.fineract.client.services.LoanProductsApi; import org.apache.fineract.client.services.LoanTransactionsApi; import org.apache.fineract.client.services.LoansApi; @@ -244,4 +245,9 @@ public class ApiConfiguration { public RescheduleLoansApi rescheduleLoansApi() { return fineractClient.createService(RescheduleLoansApi.class); } + + @Bean + public LoanInterestPauseApi loanInterestPauseApi() { + return fineractClient.createService(LoanInterestPauseApi.class); + } } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanInterestPauseStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanInterestPauseStepDef.java new file mode 100644 index 000000000..39932fdaf --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanInterestPauseStepDef.java @@ -0,0 +1,53 @@ +/** + * 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.test.stepdef.loan; + +import io.cucumber.java.en.And; +import java.io.IOException; +import org.apache.fineract.client.models.CommandProcessingResult; +import org.apache.fineract.client.models.InterestPauseRequestDto; +import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.client.services.LoanInterestPauseApi; +import org.apache.fineract.test.helper.ErrorHelper; +import org.apache.fineract.test.stepdef.AbstractStepDef; +import org.apache.fineract.test.support.TestContextKey; +import org.springframework.beans.factory.annotation.Autowired; +import retrofit2.Response; + +public class LoanInterestPauseStepDef extends AbstractStepDef { + + @Autowired + private LoanInterestPauseApi loanInterestPauseApi; + + @And("Customer creates interest pause with start date {string} and end date {string}") + public void createInterestPause(final String startDate, final String endDate) throws IOException { + final Response loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + assert loanResponse.body() != null; + final long loanId = loanResponse.body().getLoanId(); + + final InterestPauseRequestDto request = new InterestPauseRequestDto()// + .startDate(startDate)// + .endDate(endDate)// + .dateFormat("dd MMMM yyyy")// + .locale("en");// + + final Response createResponse = loanInterestPauseApi.createInterestPause(loanId, request).execute(); + ErrorHelper.checkSuccessfulApiCall(createResponse); + } +} diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanInterestPause.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanInterestPause.feature new file mode 100644 index 000000000..311e7ba25 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanInterestPause.feature @@ -0,0 +1,74 @@ +@InterestPauseFeature +Feature: Loan interest pause on repayment schedule + + Scenario: S1 - pause calculation within same period, interestRecalculation = true + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + And Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | | | | 0.0 | + | 1 | 31 | 01 February 2024 | | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100 | 2.05 | 0 | 0 | 102.05 | 0 | 0 | 0 | 102.05 | + And Admin successfully approves the loan on "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024" + And Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + When Admin sets the business date to "1 February 2024" + And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount + And Customer creates interest pause with start date "05 February 2024" and end date "10 February 2024" + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | | 66.95 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.33 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.61 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.8 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.8 | 0.1 | 0.0 | 0.0 | 16.9 | 0.0 | 0.0 | 0.0 | 16.9 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100 | 1.95 | 0 | 0 | 101.95 | 17.01 | 0 | 0 | 84.94 | + + Scenario: S2 - pause calculation between two periods, interestRecalculation = true + When Admin sets the business date to "1 January 2024" + And Admin creates a client with random data + And Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | | | | 0.0 | + | 1 | 31 | 01 February 2024 | | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 2 | 29 | 01 March 2024 | | 67.05 | 16.52 | 0.49 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 50.43 | 16.62 | 0.39 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.71 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.9 | 16.81 | 0.2 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.9 | 0.1 | 0.0 | 0.0 | 17.0 | 0.0 | 0.0 | 0.0 | 17.0 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100 | 2.05 | 0 | 0 | 102.05 | 0 | 0 | 0 | 102.05 | + And Admin successfully approves the loan on "1 January 2024" with "100" amount and expected disbursement date on "1 January 2024" + And Admin successfully disburse the loan on "1 January 2024" with "100" EUR transaction amount + When Admin sets the business date to "1 February 2024" + And Customer makes "AUTOPAY" repayment on "01 February 2024" with 17.01 EUR transaction amount + And Customer creates interest pause with start date "10 February 2024" and end date "10 March 2024" + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 01 February 2024 | 01 February 2024 | 83.57 | 16.43 | 0.58 | 0.0 | 0.0 | 17.01 | 17.01 | 0.0 | 0.0 | 0.0 | + | 2 | 29 | 01 March 2024 | | 66.69 | 16.88 | 0.13 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 3 | 31 | 01 April 2024 | | 49.96 | 16.73 | 0.28 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 4 | 30 | 01 May 2024 | | 33.24 | 16.72 | 0.29 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 5 | 31 | 01 June 2024 | | 16.42 | 16.82 | 0.19 | 0.0 | 0.0 | 17.01 | 0.0 | 0.0 | 0.0 | 17.01 | + | 6 | 30 | 01 July 2024 | | 0.0 | 16.42 | 0.1 | 0.0 | 0.0 | 16.52 | 0.0 | 0.0 | 0.0 | 16.52 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 100 | 1.57 | 0 | 0 | 101.57 | 17.01 | 0 | 0 | 84.56 | diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java index 3d5b0e520..b7e1ec1fa 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/interestpauses/service/InterestPauseWritePlatformServiceImpl.java @@ -22,6 +22,7 @@ import static org.apache.fineract.portfolio.loanaccount.domain.LoanStatus.ACTIVE import static org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType.INTEREST_PAUSE; import static org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType.PROGRESSIVE; +import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -43,6 +44,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository; +import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.springframework.transaction.annotation.Transactional; @AllArgsConstructor @@ -51,24 +53,26 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite private final LoanTermVariationsRepository loanTermVariationsRepository; private final LoanRepositoryWrapper loanRepositoryWrapper; + private final LoanAssembler loanAssembler; @Override - public CommandProcessingResult createInterestPause(ExternalId loanExternalId, String startDateString, String endDateString, - String dateFormat, String locale) { + public CommandProcessingResult createInterestPause(final ExternalId loanExternalId, final String startDateString, + final String endDateString, final String dateFormat, final String locale) { final LocalDate startDate = parseDate(startDateString, dateFormat, locale); final LocalDate endDate = parseDate(endDateString, dateFormat, locale); + final Loan loan = loanAssembler.assembleFrom(loanExternalId, false); - return processInterestPause(loanRepositoryWrapper.findOneWithNotFoundDetection(loanExternalId), startDate, endDate, dateFormat, - locale); + return processInterestPause(loan, startDate, endDate, dateFormat, locale); } @Override - public CommandProcessingResult createInterestPause(Long loanId, String startDateString, String endDateString, String dateFormat, - String locale) { + public CommandProcessingResult createInterestPause(final Long loanId, final String startDateString, final String endDateString, + final String dateFormat, final String locale) { final LocalDate startDate = parseDate(startDateString, dateFormat, locale); final LocalDate endDate = parseDate(endDateString, dateFormat, locale); + final Loan loan = loanAssembler.assembleFrom(loanId, false); - return processInterestPause(loanRepositoryWrapper.findOneWithNotFoundDetection(loanId), startDate, endDate, dateFormat, locale); + return processInterestPause(loan, startDate, endDate, dateFormat, locale); } @Override @@ -131,14 +135,18 @@ public class InterestPauseWritePlatformServiceImpl implements InterestPauseWrite .with(Map.of("startDate", startDate.toString(), "endDate", endDate.toString())).build(); } - private CommandProcessingResult processInterestPause(Loan loan, LocalDate startDate, LocalDate endDate, String dateFormat, - String locale) { + private CommandProcessingResult processInterestPause(final Loan loan, final LocalDate startDate, final LocalDate endDate, + String dateFormat, String locale) { validateActiveLoan(loan); validateInterestPauseDates(loan, startDate, endDate, dateFormat, locale, null); - LoanTermVariations variation = new LoanTermVariations(INTEREST_PAUSE.getValue(), startDate, null, endDate, false, loan); + final LoanTermVariations variation = new LoanTermVariations(INTEREST_PAUSE.getValue(), startDate, BigDecimal.ZERO, endDate, false, + loan); - LoanTermVariations savedVariation = loanTermVariationsRepository.saveAndFlush(variation); + final LoanTermVariations savedVariation = loanTermVariationsRepository.saveAndFlush(variation); + + loan.getLoanTermVariations().add(savedVariation); + loan.reprocessTransactions(); return new CommandProcessingResultBuilder().withEntityId(savedVariation.getId()).build(); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsDataWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsDataWrapper.java index 8a9320154..5b8e0e79a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsDataWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTermVariationsDataWrapper.java @@ -23,16 +23,23 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.ListIterator; +import lombok.Getter; import org.apache.fineract.infrastructure.core.service.DateUtils; public class LoanTermVariationsDataWrapper { + @Getter private final List exceptionData; private ListIterator iterator; + @Getter private final List interestRateChanges; + @Getter private final List interestRateFromInstallment; + @Getter private final List dueDateVariation; private ListIterator dueDateIterator; + @Getter + private final List interestPauseVariations; public LoanTermVariationsDataWrapper(final List exceptionData) { if (exceptionData == null) { @@ -43,6 +50,7 @@ public class LoanTermVariationsDataWrapper { this.interestRateChanges = new ArrayList<>(); this.dueDateVariation = new ArrayList<>(); this.interestRateFromInstallment = new ArrayList<>(); + this.interestPauseVariations = new ArrayList<>(); deriveLoanTermVariations(); } @@ -80,18 +88,6 @@ public class LoanTermVariationsDataWrapper { return this.dueDateIterator.previous(); } - public List getInterestRateChanges() { - return this.interestRateChanges; - } - - public List getDueDateVariation() { - return this.dueDateVariation; - } - - public List getExceptionData() { - return this.exceptionData; - } - public void setExceptionData(final List exceptionData) { clearTerms(); this.exceptionData.addAll(exceptionData); @@ -103,10 +99,7 @@ public class LoanTermVariationsDataWrapper { this.interestRateChanges.clear(); this.dueDateVariation.clear(); this.interestRateFromInstallment.clear(); - } - - public List getInterestRateFromInstallment() { - return this.interestRateFromInstallment; + this.interestPauseVariations.clear(); } public int adjustNumberOfRepayments() { @@ -133,12 +126,11 @@ public class LoanTermVariationsDataWrapper { } public boolean hasExceptionVariation(final LocalDate date, ListIterator exceptionDataListIterator) { - ListIterator iterator = exceptionDataListIterator; - return hasNext(date, iterator); + return hasNext(date, exceptionDataListIterator); } public void updateLoanTermVariationsData(final List exceptionData) { - if (this.exceptionData != null && exceptionData != null && exceptionData.size() > 0) { + if (this.exceptionData != null && exceptionData != null && !exceptionData.isEmpty()) { this.exceptionData.addAll(exceptionData); deriveLoanTermVariations(); } @@ -153,12 +145,15 @@ public class LoanTermVariationsDataWrapper { this.dueDateVariation.add(loanTermVariationsData); } else if (loanTermVariationsData.getTermVariationType().isInterestRateFromInstallment()) { this.interestRateFromInstallment.add(loanTermVariationsData); + } else if (loanTermVariationsData.getTermVariationType().isInterestPauseVariation()) { + this.interestPauseVariations.add(loanTermVariationsData); } } Collections.sort(this.dueDateVariation); this.exceptionData.removeAll(this.interestRateChanges); this.exceptionData.removeAll(this.dueDateVariation); this.exceptionData.removeAll(this.interestRateFromInstallment); + this.exceptionData.removeAll(this.interestPauseVariations); this.iterator = this.exceptionData.listIterator(); this.dueDateIterator = this.dueDateVariation.listIterator(); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java index 21e4655d3..67fd9bf1f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java @@ -98,6 +98,10 @@ public enum LoanTermVariationType { return this.value.equals(LoanTermVariationType.INTEREST_RATE.getValue()); } + public boolean isInterestPauseVariation() { + return this.value.equals(LoanTermVariationType.INTEREST_PAUSE.getValue()); + } + public boolean isPrincipalAmountVariation() { return this.value.equals(LoanTermVariationType.PRINCIPAL_AMOUNT.getValue()); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index d8c12da83..59f8dc92b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.util.Map; import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.portfolio.fund.domain.Fund; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -30,6 +31,12 @@ public interface LoanAssembler { Loan assembleFrom(Long accountId); + Loan assembleFrom(Long accountId, boolean loadLazyCollections); + + Loan assembleFrom(ExternalId externalId); + + Loan assembleFrom(ExternalId externalId, boolean loadLazyCollections); + Loan assembleFrom(JsonCommand command); void setHelpers(Loan loanAccount); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 57b50552b..e743db6fd 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -261,7 +261,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) { final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); - emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate); + if (interestRateChange.getTermVariationType().isInterestPauseVariation()) { + final LocalDate pauseEndDate = interestRateChange.getDateValue(); + emiCalculator.applyInterestPause(scheduleModel, interestRateChangeSubmittedOnDate, pauseEndDate); + } else { + emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate); + } processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments); } @@ -820,9 +825,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep @NotNull private List createSortedChangeList(final LoanTermVariationsDataWrapper loanTermVariations, final List loanTransactions, final Set charges) { - List changeOperations = new ArrayList<>(); - if (loanTermVariations != null && !loanTermVariations.getInterestRateFromInstallment().isEmpty()) { - changeOperations.addAll(loanTermVariations.getInterestRateFromInstallment().stream().map(ChangeOperation::new).toList()); + final List changeOperations = new ArrayList<>(); + if (loanTermVariations != null) { + if (!loanTermVariations.getInterestPauseVariations().isEmpty()) { + changeOperations.addAll(loanTermVariations.getInterestPauseVariations().stream().map(ChangeOperation::new).toList()); + } + if (!loanTermVariations.getInterestRateFromInstallment().isEmpty()) { + changeOperations.addAll(loanTermVariations.getInterestRateFromInstallment().stream().map(ChangeOperation::new).toList()); + } } if (charges != null) { changeOperations.addAll(charges.stream().map(ChangeOperation::new).toList()); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java index 3d4824195..d21b1defa 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestPeriod.java @@ -23,6 +23,7 @@ import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; import java.util.Optional; +import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -34,6 +35,7 @@ import org.apache.fineract.organisation.monetary.domain.Money; @Getter @ToString(exclude = { "repaymentPeriod" }) @EqualsAndHashCode(exclude = { "repaymentPeriod" }) +@AllArgsConstructor public class InterestPeriod implements Comparable { private final RepaymentPeriod repaymentPeriod; @@ -49,25 +51,13 @@ public class InterestPeriod implements Comparable { private Money balanceCorrectionAmount; private Money outstandingLoanBalance; private final MathContext mc; - - public InterestPeriod(RepaymentPeriod repaymentPeriod, LocalDate fromDate, LocalDate dueDate, BigDecimal rateFactor, - BigDecimal rateFactorTillPeriodDueDate, Money disbursementAmount, Money balanceCorrectionAmount, Money outstandingLoanBalance, - MathContext mc) { - this.repaymentPeriod = repaymentPeriod; - this.fromDate = fromDate; - this.dueDate = dueDate; - this.rateFactor = rateFactor; - this.rateFactorTillPeriodDueDate = rateFactorTillPeriodDueDate; - this.disbursementAmount = disbursementAmount; - this.balanceCorrectionAmount = balanceCorrectionAmount; - this.outstandingLoanBalance = outstandingLoanBalance; - this.mc = mc; - } + private final boolean isPaused; public InterestPeriod(RepaymentPeriod repaymentPeriod, InterestPeriod interestPeriod) { this(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getDisbursementAmount(), - interestPeriod.getBalanceCorrectionAmount(), interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc()); + interestPeriod.getBalanceCorrectionAmount(), interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), + interestPeriod.isPaused()); } @Override @@ -84,6 +74,10 @@ public class InterestPeriod implements Comparable { } public Money getCalculatedDueInterest() { + if (isPaused) { + return Money.zero(outstandingLoanBalance.getCurrencyData(), mc); + } + long lengthTillPeriodDueDate = getLengthTillPeriodDueDate(); final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 // ? BigDecimal.ZERO // diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java index 543ecb776..8c9e88091 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java @@ -160,6 +160,24 @@ public class ProgressiveLoanInterestScheduleModel { .findFirst();// } + public Optional updateInterestPeriodsForInterestPause(final LocalDate fromDate, final LocalDate endDate) { + if (fromDate == null || endDate == null) { + return Optional.empty(); + } + + final List affectedPeriods = repaymentPeriods.stream()// + .filter(period -> isPeriodInRange(period, fromDate, endDate))// + .toList(); + affectedPeriods.forEach(period -> insertInterestPausePeriods(period, fromDate, endDate)); + + return affectedPeriods.stream().findFirst(); + } + + private boolean isPeriodInRange(final RepaymentPeriod repaymentPeriod, final LocalDate fromDate, final LocalDate endDate) { + return DateUtils.isDateInRangeFromExclusiveToInclusive(fromDate, repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()) + || DateUtils.isDateInRangeFromExclusiveToInclusive(endDate, repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()); + } + Optional findRepaymentPeriodForBalanceChange(final LocalDate balanceChangeDate) { if (balanceChangeDate == null) { return Optional.empty(); @@ -192,7 +210,8 @@ public class ProgressiveLoanInterestScheduleModel { }; } - Optional findInterestPeriodForBalanceChange(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate) { + private Optional findInterestPeriodForBalanceChange(final RepaymentPeriod repaymentPeriod, + final LocalDate balanceChangeDate) { if (repaymentPeriod == null || balanceChangeDate == null) { return Optional.empty(); } @@ -203,26 +222,63 @@ public class ProgressiveLoanInterestScheduleModel { void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate, final Money disbursedAmount, final Money correctionAmount) { - final InterestPeriod previousInterestPeriod; - if (balanceChangeDate.isAfter(repaymentPeriod.getFromDate())) { - previousInterestPeriod = repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1);// - } else { - previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream() - .filter(ip -> balanceChangeDate.isAfter(ip.getFromDate()) && !balanceChangeDate.isAfter(ip.getDueDate()))// - .reduce((first, second) -> second)// - .orElse(repaymentPeriod.getInterestPeriods().get(0)); - } - LocalDate originalDueDate = previousInterestPeriod.getDueDate(); - LocalDate newDueDate = balanceChangeDate.isBefore(previousInterestPeriod.getFromDate()) ? previousInterestPeriod.getFromDate() - : balanceChangeDate.isAfter(previousInterestPeriod.getDueDate()) ? previousInterestPeriod.getDueDate() : balanceChangeDate; + final InterestPeriod previousInterestPeriod = findPreviousInterestPeriod(repaymentPeriod, balanceChangeDate); + final LocalDate originalDueDate = previousInterestPeriod.getDueDate(); + final LocalDate newDueDate = calculateNewDueDate(previousInterestPeriod, balanceChangeDate); + previousInterestPeriod.setDueDate(newDueDate); previousInterestPeriod.addDisbursementAmount(disbursedAmount); previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount); - final InterestPeriod interestPeriod = new InterestPeriod(repaymentPeriod, previousInterestPeriod.getDueDate(), originalDueDate, - BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, mc); + + final InterestPeriod interestPeriod = new InterestPeriod(repaymentPeriod, newDueDate, originalDueDate, BigDecimal.ZERO, + BigDecimal.ZERO, zero, zero, zero, mc, false); repaymentPeriod.getInterestPeriods().add(interestPeriod); } + private void insertInterestPausePeriods(final RepaymentPeriod repaymentPeriod, final LocalDate fromDate, final LocalDate endDate) { + final InterestPeriod previousInterestPeriod = findPreviousInterestPeriod(repaymentPeriod, fromDate); + final LocalDate originalFromDate = previousInterestPeriod.getFromDate(); + final LocalDate originalDueDate = previousInterestPeriod.getDueDate(); + final LocalDate newDueDate = calculateNewDueDate(previousInterestPeriod, fromDate.minusDays(1)); + + if (fromDate.isAfter(originalFromDate) && endDate.isBefore(originalDueDate)) { + previousInterestPeriod.setDueDate(newDueDate); + final InterestPeriod interestPausePeriod = new InterestPeriod(repaymentPeriod, newDueDate, endDate, BigDecimal.ZERO, + BigDecimal.ZERO, zero, zero, zero, mc, true); + repaymentPeriod.getInterestPeriods().add(interestPausePeriod); + final InterestPeriod interestAfterPausePeriod = new InterestPeriod(repaymentPeriod, endDate, originalDueDate, BigDecimal.ZERO, + BigDecimal.ZERO, zero, zero, zero, mc, false); + repaymentPeriod.getInterestPeriods().add(interestAfterPausePeriod); + } + + if (fromDate.isAfter(originalFromDate) && endDate.isAfter(originalDueDate)) { + previousInterestPeriod.setDueDate(newDueDate); + final InterestPeriod interestPausePeriod = new InterestPeriod(repaymentPeriod, newDueDate, originalDueDate, BigDecimal.ZERO, + BigDecimal.ZERO, zero, zero, zero, mc, true); + repaymentPeriod.getInterestPeriods().add(interestPausePeriod); + } + + if (fromDate.isBefore(originalFromDate) && endDate.isBefore(originalDueDate)) { + repaymentPeriod.getInterestPeriods().clear(); + final InterestPeriod interestPausePeriod = new InterestPeriod(repaymentPeriod, newDueDate, originalDueDate, BigDecimal.ZERO, + BigDecimal.ZERO, zero, zero, zero, mc, true); + repaymentPeriod.getInterestPeriods().add(interestPausePeriod); + InterestPeriod interestAfterPausePeriod = new InterestPeriod(repaymentPeriod, endDate, originalDueDate, BigDecimal.ZERO, + BigDecimal.ZERO, zero, zero, zero, mc, false); + repaymentPeriod.getInterestPeriods().add(interestAfterPausePeriod); + } + } + + private InterestPeriod findPreviousInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate date) { + if (date.isAfter(repaymentPeriod.getFromDate())) { + return repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1); + } else { + return repaymentPeriod.getInterestPeriods().stream() + .filter(ip -> date.isAfter(ip.getFromDate()) && !date.isAfter(ip.getDueDate())).reduce((first, second) -> second) + .orElse(repaymentPeriod.getInterestPeriods().get(0)); + } + } + public Money getTotalDueInterest() { return repaymentPeriods().stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest)) .reduce(zero(), Money::plus); @@ -282,4 +338,10 @@ public class ProgressiveLoanInterestScheduleModel { } } } + + private LocalDate calculateNewDueDate(final InterestPeriod previousInterestPeriod, final LocalDate date) { + return date.isBefore(previousInterestPeriod.getFromDate()) ? previousInterestPeriod.getFromDate() + : date.isAfter(previousInterestPeriod.getDueDate()) ? previousInterestPeriod.getDueDate() : date; + } + } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java index 5ecd47ca6..9edba49c0 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java @@ -73,7 +73,7 @@ public class RepaymentPeriod { this.interestPeriods = new ArrayList<>(); // There is always at least 1 interest period, by default with same from-due date as repayment period getInterestPeriods().add(new InterestPeriod(this, getFromDate(), getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, getZero(mc), - getZero(mc), getZero(mc), mc)); + getZero(mc), getZero(mc), mc, false)); this.paidInterest = getZero(mc); this.paidPrincipal = getZero(mc); } 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 d6053752c..18126360b 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 @@ -209,16 +209,22 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { private void applyInterestRateChangesOnPeriod(final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModelRepaymentPeriod repaymentPeriod, final ProgressiveLoanInterestScheduleModel interestScheduleModel) { - if (loanApplicationTerms.getLoanTermVariations() != null) { - for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) { - final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); - final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); - if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate()) - && !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) { - emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate); - } - } + if (loanApplicationTerms.getLoanTermVariations() == null) { + return; } + + loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment().stream() + .filter(change -> isDateWithinPeriod(change.getTermVariationApplicableFrom(), repaymentPeriod)) + .forEach(change -> emiCalculator.changeInterestRate(interestScheduleModel, change.getTermVariationApplicableFrom(), + change.getDecimalValue())); + + loanApplicationTerms.getLoanTermVariations().getInterestPauseVariations().stream() + .filter(pause -> isDateWithinPeriod(pause.getTermVariationApplicableFrom(), repaymentPeriod)).forEach(pause -> emiCalculator + .applyInterestPause(interestScheduleModel, pause.getTermVariationApplicableFrom(), pause.getDateValue())); + } + + private boolean isDateWithinPeriod(final LocalDate date, final LoanScheduleModelRepaymentPeriod period) { + return date.isAfter(period.getFromDate()) && !date.isAfter(period.getDueDate()); } private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java index 193e859da..f9b206a69 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -77,4 +77,6 @@ public interface EMICalculator { OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel model, LocalDate targetDate); Money getSumOfDueInterestsOnDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate subjectDate); + + void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index e9d67f72d..fa349cbf0 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -792,6 +792,20 @@ public final class ProgressiveEMICalculator implements EMICalculator { .reduce(scheduleModel.zero(), Money::add); // } + @Override + public void applyInterestPause(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate fromDate, + final LocalDate endDate) { + scheduleModel.updateInterestPeriodsForInterestPause(fromDate, endDate) + .ifPresent(repaymentPeriod -> calculateRateFactorsForInterestPause(scheduleModel, repaymentPeriod.getFromDate())); + } + + private void calculateRateFactorsForInterestPause(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate startDate) { + final List relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(startDate); + calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel); + calculateOutstandingBalance(scheduleModel); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); + } + private long getUncountablePeriods(final List relatedRepaymentPeriods, final Money originalEmi) { return relatedRepaymentPeriods.stream() // .filter(repaymentPeriod -> originalEmi.isLessThan(repaymentPeriod.getTotalPaidAmount())) // 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 852e778a6..694cb11c9 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 @@ -1342,6 +1342,82 @@ class ProgressiveEMICalculatorTest { checkDailyInterest(interestModel, dueDate, startDay, 31, 0.38, 9.03); } + @Test + public void test_singleInterestPauseAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), + repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)), + repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)), + repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)), + repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)), + repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(7.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).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.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); + emiCalculator.applyInterestPause(interestSchedule, LocalDate.of(2024, 2, 5), LocalDate.of(2024, 2, 10)); + + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.000603448276, 0.05, 0.39, 16.62, 66.95); + checkPeriod(interestSchedule, 1, 1, 17.01, 0.001206896552, 0.0, 0.39, 16.62, 66.95); + checkPeriod(interestSchedule, 1, 2, 17.01, 0.004022988506, 0.34, 0.39, 16.62, 66.95); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 0.39, 16.62, 50.33); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 0.29, 16.72, 33.61); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.2, 0.2, 16.81, 16.8); + checkPeriod(interestSchedule, 5, 0, 16.9, 0.005833333333, 0.1, 0.1, 16.8, 0.0); + } + + @Test + public void test_interestPauseBetweenTwoPeriodsAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), + repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)), + repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)), + repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)), + repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)), + repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(7.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).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.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, loanTermVariations, installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); + emiCalculator.applyInterestPause(interestSchedule, LocalDate.of(2024, 2, 10), LocalDate.of(2024, 3, 10)); + + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.001609195402, 0.13, 0.13, 16.88, 66.69); + checkPeriod(interestSchedule, 1, 1, 17.01, 0.004224137931, 0.0, 0.13, 16.88, 66.69); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.0, 0.28, 16.73, 49.96); + checkPeriod(interestSchedule, 2, 1, 17.01, 0.004139784946, 0.28, 0.28, 16.73, 49.96); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 0.29, 16.72, 33.24); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.19, 0.19, 16.82, 16.42); + checkPeriod(interestSchedule, 5, 0, 16.52, 0.005833333333, 0.1, 0.1, 16.42, 0.0); + } + @Test public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = List.of( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java index 8c08bae3a..d56285305 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java @@ -139,15 +139,36 @@ public class LoanAssemblerImpl implements LoanAssembler { @Override public Loan assembleFrom(final Long accountId) { - final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); - loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanRepaymentScheduleTransactionProcessorFactory); + return assembleFrom(accountId, true); + } + + @Override + public Loan assembleFrom(final Long accountId, final boolean loadLazyCollections) { + final Loan loanAccount = loanRepository.findOneWithNotFoundDetection(accountId, loadLazyCollections); + setHelpers(loanAccount); + + return loanAccount; + } + + @Override + public Loan assembleFrom(final ExternalId externalId) { + final Loan loanAccount = loanRepository.findOneWithNotFoundDetection(externalId, true); + setHelpers(loanAccount); + + return loanAccount; + } + + @Override + public Loan assembleFrom(final ExternalId externalId, final boolean loadLazyCollections) { + final Loan loanAccount = loanRepository.findOneWithNotFoundDetection(externalId, loadLazyCollections); + setHelpers(loanAccount); return loanAccount; } @Override public void setHelpers(final Loan loanAccount) { - loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanRepaymentScheduleTransactionProcessorFactory); + loanAccount.setHelpers(defaultLoanLifecycleStateMachine, loanRepaymentScheduleTransactionProcessorFactory); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index b7b9a955d..1c1e75fe7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -471,7 +471,7 @@ public class LoanAccountConfiguration { @Bean @ConditionalOnMissingBean(InterestPauseWritePlatformService.class) public InterestPauseWritePlatformService interestPauseWritePlatformService(LoanTermVariationsRepository loanTermVariationsRepository, - LoanRepositoryWrapper loanRepositoryWrapper) { - return new InterestPauseWritePlatformServiceImpl(loanTermVariationsRepository, loanRepositoryWrapper); + LoanRepositoryWrapper loanRepositoryWrapper, LoanAssembler loanAssembler) { + return new InterestPauseWritePlatformServiceImpl(loanTermVariationsRepository, loanRepositoryWrapper, loanAssembler); } }