FINERACT-2152: Interest pause during one period and between two periods
This commit is contained in:
committed by
Adam Saghy
parent
a9d76ef3dd
commit
af37e07f31
@@ -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() {
|
||||
|
||||
+6
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+53
@@ -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<PostLoansResponse> 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<CommandProcessingResult> createResponse = loanInterestPauseApi.createInterestPause(loanId, request).execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(createResponse);
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
+19
-11
@@ -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();
|
||||
}
|
||||
|
||||
+14
-19
@@ -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<LoanTermVariationsData> exceptionData;
|
||||
private ListIterator<LoanTermVariationsData> iterator;
|
||||
@Getter
|
||||
private final List<LoanTermVariationsData> interestRateChanges;
|
||||
@Getter
|
||||
private final List<LoanTermVariationsData> interestRateFromInstallment;
|
||||
@Getter
|
||||
private final List<LoanTermVariationsData> dueDateVariation;
|
||||
private ListIterator<LoanTermVariationsData> dueDateIterator;
|
||||
@Getter
|
||||
private final List<LoanTermVariationsData> interestPauseVariations;
|
||||
|
||||
public LoanTermVariationsDataWrapper(final List<LoanTermVariationsData> 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<LoanTermVariationsData> getInterestRateChanges() {
|
||||
return this.interestRateChanges;
|
||||
}
|
||||
|
||||
public List<LoanTermVariationsData> getDueDateVariation() {
|
||||
return this.dueDateVariation;
|
||||
}
|
||||
|
||||
public List<LoanTermVariationsData> getExceptionData() {
|
||||
return this.exceptionData;
|
||||
}
|
||||
|
||||
public void setExceptionData(final List<LoanTermVariationsData> exceptionData) {
|
||||
clearTerms();
|
||||
this.exceptionData.addAll(exceptionData);
|
||||
@@ -103,10 +99,7 @@ public class LoanTermVariationsDataWrapper {
|
||||
this.interestRateChanges.clear();
|
||||
this.dueDateVariation.clear();
|
||||
this.interestRateFromInstallment.clear();
|
||||
}
|
||||
|
||||
public List<LoanTermVariationsData> getInterestRateFromInstallment() {
|
||||
return this.interestRateFromInstallment;
|
||||
this.interestPauseVariations.clear();
|
||||
}
|
||||
|
||||
public int adjustNumberOfRepayments() {
|
||||
@@ -133,12 +126,11 @@ public class LoanTermVariationsDataWrapper {
|
||||
}
|
||||
|
||||
public boolean hasExceptionVariation(final LocalDate date, ListIterator<LoanTermVariationsData> exceptionDataListIterator) {
|
||||
ListIterator<LoanTermVariationsData> iterator = exceptionDataListIterator;
|
||||
return hasNext(date, iterator);
|
||||
return hasNext(date, exceptionDataListIterator);
|
||||
}
|
||||
|
||||
public void updateLoanTermVariationsData(final List<LoanTermVariationsData> 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();
|
||||
}
|
||||
|
||||
+4
@@ -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());
|
||||
}
|
||||
|
||||
+7
@@ -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);
|
||||
|
||||
+14
-4
@@ -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<ChangeOperation> createSortedChangeList(final LoanTermVariationsDataWrapper loanTermVariations,
|
||||
final List<LoanTransaction> loanTransactions, final Set<LoanCharge> charges) {
|
||||
List<ChangeOperation> changeOperations = new ArrayList<>();
|
||||
if (loanTermVariations != null && !loanTermVariations.getInterestRateFromInstallment().isEmpty()) {
|
||||
changeOperations.addAll(loanTermVariations.getInterestRateFromInstallment().stream().map(ChangeOperation::new).toList());
|
||||
final List<ChangeOperation> 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());
|
||||
|
||||
+9
-15
@@ -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<InterestPeriod> {
|
||||
|
||||
private final RepaymentPeriod repaymentPeriod;
|
||||
@@ -49,25 +51,13 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
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<InterestPeriod> {
|
||||
}
|
||||
|
||||
public Money getCalculatedDueInterest() {
|
||||
if (isPaused) {
|
||||
return Money.zero(outstandingLoanBalance.getCurrencyData(), mc);
|
||||
}
|
||||
|
||||
long lengthTillPeriodDueDate = getLengthTillPeriodDueDate();
|
||||
final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 //
|
||||
? BigDecimal.ZERO //
|
||||
|
||||
+77
-15
@@ -160,6 +160,24 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
.findFirst();//
|
||||
}
|
||||
|
||||
public Optional<RepaymentPeriod> updateInterestPeriodsForInterestPause(final LocalDate fromDate, final LocalDate endDate) {
|
||||
if (fromDate == null || endDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final List<RepaymentPeriod> 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<RepaymentPeriod> findRepaymentPeriodForBalanceChange(final LocalDate balanceChangeDate) {
|
||||
if (balanceChangeDate == null) {
|
||||
return Optional.empty();
|
||||
@@ -192,7 +210,8 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
};
|
||||
}
|
||||
|
||||
Optional<InterestPeriod> findInterestPeriodForBalanceChange(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate) {
|
||||
private Optional<InterestPeriod> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+15
-9
@@ -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) {
|
||||
|
||||
+2
@@ -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);
|
||||
}
|
||||
|
||||
+14
@@ -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<RepaymentPeriod> relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(startDate);
|
||||
calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel);
|
||||
calculateOutstandingBalance(scheduleModel);
|
||||
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel);
|
||||
}
|
||||
|
||||
private long getUncountablePeriods(final List<RepaymentPeriod> relatedRepaymentPeriods, final Money originalEmi) {
|
||||
return relatedRepaymentPeriods.stream() //
|
||||
.filter(repaymentPeriod -> originalEmi.isLessThan(repaymentPeriod.getTotalPaidAmount())) //
|
||||
|
||||
+76
@@ -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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
|
||||
+24
-3
@@ -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
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user