Skip to content

Commit

Permalink
FINERACT-2163: Fix for properly considering 30 days in a month settin…
Browse files Browse the repository at this point in the history
…g when calculating periodic interest
  • Loading branch information
galovics committed Jan 7, 2025
1 parent dfc1d3e commit a2b67e1
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 7 deletions.
2 changes: 1 addition & 1 deletion docker-compose-web-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ version: "3.8"
services:
# Frontend service
community-app:
image: openmf/web-app:latest
image: openmf/web-app:master
container_name: mifos-web-app
restart: always
ports:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1027,6 +1027,9 @@ public BigDecimal calculatePeriodsBetweenDates(final LocalDate startDate, final
int daysLeftAfterMonths = DateUtils.getExactDifferenceInDays(startDateAfterConsideringMonths, endDate) + diffDays;
int daysInPeriodAfterMonths = DateUtils.getExactDifferenceInDays(startDateAfterConsideringMonths,
endDateAfterConsideringMonths);
if (loanCalendar == null && daysInMonthType.isDaysInMonth_30()) {
daysInPeriodAfterMonths = 30;
}
numberOfPeriods = numberOfPeriods.add(BigDecimal.valueOf(numberOfMonths))
.add(BigDecimal.valueOf((double) daysLeftAfterMonths / daysInPeriodAfterMonths));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1281,14 +1281,16 @@ private PostLoansRequest() {}
public List<PostLoansDisbursementData> disbursementData;
@Schema(description = "Maximum allowed outstanding balance")
public BigDecimal maxOutstandingLoanBalance;
@Schema(example = "[2011, 10, 20]")
public LocalDate repaymentsStartingFromDate;
@Schema(example = "20 September 2011")
public String repaymentsStartingFromDate;
@Schema(example = "1")
public Integer graceOnInterestCharged;
@Schema(example = "1")
public Integer graceOnPrincipalPayment;
@Schema(example = "1")
public Integer graceOnInterestPayment;
@Schema(example = "20 September 2011")
public String interestChargedFromDate;
@Schema(example = "1")
public Integer graceOnArrearsAgeing;
@Schema(example = "HORIZONTAL")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,13 @@ protected Long createDisbursementPercentageCharge(double percentageAmount) {
return chargeId.longValue();
}

protected Long createDisbursementFlatCharge(double amount) {
Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
ChargesHelper.getLoanDisbursementJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(amount)));
assertNotNull(chargeId);
return chargeId.longValue();
}

protected void verifyRepaymentSchedule(GetLoansLoanIdResponse savedLoanResponse, GetLoansLoanIdResponse actualLoanResponse,
int totalPeriods, int identicalPeriods) {
List<GetLoansLoanIdRepaymentPeriod> savedPeriods = savedLoanResponse.getRepaymentSchedule().getPeriods();
Expand Down Expand Up @@ -749,14 +756,24 @@ private void verifyPeriodsEquality(List<GetLoansLoanIdRepaymentPeriod> savedPeri
}
}

protected void verifyRepaymentSchedulePartially(Long loanId, Installment... installments) {
verifyRepaymentSchedule(true, loanId, installments);
}

protected void verifyRepaymentSchedule(Long loanId, Installment... installments) {
verifyRepaymentSchedule(false, loanId, installments);
}

protected void verifyRepaymentSchedule(boolean partialMatchingOnly, Long loanId, Installment... installments) {
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);

assertNotNull(loanResponse.getRepaymentSchedule());
assertNotNull(loanResponse.getRepaymentSchedule().getPeriods());
Assertions.assertEquals(installments.length, loanResponse.getRepaymentSchedule().getPeriods().size(),
"Expected installments are not matching with the installments configured on the loan");
if (!partialMatchingOnly) {
Assertions.assertEquals(installments.length, loanResponse.getRepaymentSchedule().getPeriods().size(),
"Expected installments are not matching with the installments configured on the loan");
}

int installmentNumber = 0;
for (int i = 0; i < installments.length; i++) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.integrationtests;

import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.InterestCalculationPeriodType.DAILY;

import java.math.BigDecimal;
import java.util.List;
import org.apache.fineract.client.models.ChargeData;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansRequestChargeData;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.junit.jupiter.api.Test;

public class LoanDailyInterestTest extends BaseLoanIntegrationTest {

@Test
public void test_LoanInterestIsCalculatedFor30Days_WhenInterestIsDaily_AndDaysInMonthIs30_AndRepaymentStartsIn5Days_AndInterestIsChargedFromDisbursementDate_AndChargeIsPresent() {
runAt("01 August 2024", () -> {
int amortizationType = AmortizationType.EQUAL_INSTALLMENTS;
int interestType = InterestType.DECLINING_BALANCE;

double chargeAmount = 28_161.24;
double amount = 532_770.0;
int numberOfRepayments = 60;

Long chargeId = createDisbursementFlatCharge(chargeAmount);

// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();

// Create Loan Product
PostLoanProductsRequest product = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
interestType, amortizationType)//
.transactionProcessingStrategyCode(LoanProductTestBuilder.DEFAULT_STRATEGY)//
.installmentAmountInMultiplesOf(null)//
.maxPrincipal(amount).minPrincipal(amount).principal(amount).minNumberOfRepayments(numberOfRepayments)
.numberOfRepayments(numberOfRepayments).maxNumberOfRepayments(numberOfRepayments).maxInterestRatePerPeriod(12.0)//
.minInterestRatePerPeriod(12.0)//
.interestRatePerPeriod(12.0)//
.interestRateFrequencyType(InterestRateFrequencyType.YEARS)//
.interestCalculationPeriodType(DAILY)//
.allowPartialPeriodInterestCalcualtion(false)//
.isInterestRecalculationEnabled(true)//
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
.recalculationRestFrequencyInterval(1) //
.recalculationCompoundingFrequencyType(1) //
.preClosureInterestCalculationStrategy(1)//
.daysInMonthType(DaysInMonthType.DAYS_30)//
.daysInYearType(DaysInYearType.DAYS_360)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
.interestRecalculationCompoundingMethod(0)//
.charges(List.of(new ChargeData().id(chargeId)));//

PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
Long loanProductId = loanProductResponse.getResourceId();

// Apply and Approve Loan

PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 August 2024", amount, numberOfRepayments)//
.repaymentEvery(1)//
.loanTermFrequency(numberOfRepayments)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
.interestType(interestType)//
.amortizationType(amortizationType)//
.interestCalculationPeriodType(DAILY)//
.repaymentsStartingFromDate("05 August 2024")//
.interestChargedFromDate("01 August 2024").interestRatePerPeriod(BigDecimal.valueOf(12))//
.charges(List.of(new PostLoansRequestChargeData().chargeId(chargeId).amount(BigDecimal.valueOf(chargeAmount))));

PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);

PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(amount, "01 August 2024"));

Long loanId = approvedLoanResult.getLoanId();

// Verify Repayment Schedule
verifyRepaymentSchedulePartially(loanId, //
installment(532_770.0, null, "01 August 2024"), //
installment(11_140.81, 710.36, 11_851.17, false, "05 August 2024"), //
installment(6_634.88, 5_216.29, 11_851.17, false, "05 September 2024"), //
installment(6_701.23, 5_149.94, 11_851.17, false, "05 October 2024") //
);

// disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(532_770.0), "01 August 2024");

// Verify Repayment Schedule
verifyRepaymentSchedulePartially(loanId, //
installment(532_770.0, null, "01 August 2024"), //
installment(11_140.81, 710.36, 11_851.17, false, "05 August 2024"), //
installment(6_634.88, 5_216.29, 11_851.17, false, "05 September 2024"), //
installment(6_701.23, 5_149.94, 11_851.17, false, "05 October 2024") //
);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.stream.Stream;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
Expand Down Expand Up @@ -60,7 +59,7 @@ public void dueDateBasedOnFirstRepaymentDate(String repaymentProcessor) {
(postLoansRequest) -> {
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
.loanTermFrequency(4).loanTermFrequencyType(2).dateFormat("yyyy-MM-dd")
.repaymentsStartingFromDate(LocalDate.of(2024, 2, 29));
.repaymentsStartingFromDate("29 February 2024");
});
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(loanRequest);
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
Expand Down

0 comments on commit a2b67e1

Please sign in to comment.