FINERACT-1981: fix progressive accruals on late repayment

This commit is contained in:
adam.magyari
2025-02-04 11:47:52 +01:00
committed by Adam Saghy
parent 0b9687d2aa
commit 157a492bcc
5 changed files with 220 additions and 7 deletions
@@ -217,6 +217,10 @@ public final class MathUtil {
return nullToZero(first).compareTo(nullToZero(second)) < 0;
}
public static boolean isLessThanOrEqualTo(BigDecimal first, BigDecimal second) {
return nullToZero(first).compareTo(second) <= 0;
}
public static boolean isGreaterThanOrEqualTo(BigDecimal first, BigDecimal second) {
return nullToZero(first).compareTo(nullToZero(second)) >= 0;
}
@@ -5335,3 +5335,128 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan
| 22 January 2021 | Accrual | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | false |
Then In Loan Transactions the "2"th Transaction has Transaction type="Merchant Issued Refund" and is reverted
Then In Loan Transactions the "3"th Transaction has Transaction type="Interest Refund" and is reverted
@TestRailId:C3313
Scenario: Verify that due date charges after maturity date is recognized on repayment schedule
When Global config "charge-accrual-date" value set to "submitted-date"
When Admin sets the business date to "01 January 2024"
And Admin creates a client with random data
And Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
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_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
Then Loan Repayment schedule has 4 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 | | 75.21 | 24.79 | 0.58 | 0.0 | 0.0 | 25.37 | 0.0 | 0.0 | 0.0 | 25.37 |
| 2 | 29 | 01 March 2024 | | 50.28 | 24.93 | 0.44 | 0.0 | 0.0 | 25.37 | 0.0 | 0.0 | 0.0 | 25.37 |
| 3 | 31 | 01 April 2024 | | 25.2 | 25.08 | 0.29 | 0.0 | 0.0 | 25.37 | 0.0 | 0.0 | 0.0 | 25.37 |
| 4 | 30 | 01 May 2024 | | 0.0 | 25.2 | 0.15 | 0.0 | 0.0 | 25.35 | 0.0 | 0.0 | 0.0 | 25.35 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 1.46 | 0.0 | 0.0 | 101.46 | 0.0 | 0.0 | 0.0 | 101.46 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
When Admin sets the business date to "1 May 2024"
And Admin runs inline COB job for Loan
And Admin adds "LOAN_NSF_FEE" due date charge with "15 May 2024" due date and 10 EUR transaction amount
Then Loan Repayment schedule has 5 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 | | 75.21 | 24.79 | 0.58 | 0.0 | 0.0 | 25.37 | 0.0 | 0.0 | 0.0 | 25.37 |
| 2 | 29 | 01 March 2024 | | 50.42 | 24.79 | 0.58 | 0.0 | 0.0 | 25.37 | 0.0 | 0.0 | 0.0 | 25.37 |
| 3 | 31 | 01 April 2024 | | 25.63 | 24.79 | 0.58 | 0.0 | 0.0 | 25.37 | 0.0 | 0.0 | 0.0 | 25.37 |
| 4 | 30 | 01 May 2024 | | 0.0 | 25.63 | 0.58 | 0.0 | 0.0 | 26.21 | 0.0 | 0.0 | 0.0 | 26.21 |
| 5 | 14 | 15 May 2024 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 2.32 | 0.0 | 10.0 | 112.32 | 0.0 | 0.0 | 0.0 | 112.32 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 30 April 2024 | Accrual | 1.87 | 0.0 | 1.87 | 0.0 | 0.0 | 0.0 | false | false |
And Loan Charges tab has the following data:
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
| NSF fee | true | Specified due date | 15 May 2024 | Flat | 10.0 | 0.0 | 0.0 | 10.0 |
When Admin sets the business date to "02 May 2024"
And Admin runs inline COB job for Loan
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 30 April 2024 | Accrual | 1.87 | 0.0 | 1.87 | 0.0 | 0.0 | 0.0 | false | false |
| 01 May 2024 | Accrual | 10.45 | 0.0 | 0.45 | 0.0 | 10.0 | 0.0 | false | false |
@TestRailId:C3333
Scenario: Verify that due date charges after maturity date with inline COB run is recognized on repayment schedule
When Admin sets the business date to "01 January 2024"
And Admin creates a client with random data
And Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
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_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE | 01 January 2024 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 1 | MONTHS | 1 | MONTHS | 1 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "100" amount and expected disbursement date on "01 January 2024"
And Admin successfully disburse the loan on "01 January 2024" with "100" EUR transaction amount
And Admin runs inline COB job for Loan
Then Loan Repayment schedule has 1 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 | | 0.0 | 100.0 | 0.58 | 0.0 | 0.0 | 100.58 | 0.0 | 0.0 | 0.0 | 100.58 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 0.58 | 0.0 | 0.0 | 100.58 | 0.0 | 0.0 | 0.0 | 100.58 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
When Admin sets the business date to "15 February 2024"
And Admin adds "LOAN_NSF_FEE" due date charge with "15 February 2024" due date and 10 EUR transaction amount
When Admin sets the business date to "16 February 2024"
And Admin runs inline COB job for Loan
Then Loan Repayment schedule has 2 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 | | 0.0 | 100.0 | 0.58 | 0.0 | 0.0 | 100.58 | 0.0 | 0.0 | 0.0 | 100.58 |
| 2 | 14 | 15 February 2024 | | 0.0 | 0.0 | 0.0 | 0.0 | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 |
And Loan Repayment schedule has the following data in Total row:
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| 100.0 | 0.58 | 0.0 | 10.0 | 110.58 | 0.0 | 0.0 | 0.0 | 110.58 |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
| 02 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 03 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 04 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 05 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 06 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false |
| 07 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 08 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 09 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 10 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 11 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 12 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 13 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 14 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false |
| 15 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 16 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 17 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 18 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 19 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 20 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 21 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 22 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 23 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false |
| 24 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 25 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 26 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 27 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 28 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 29 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 30 January 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 31 January 2024 | Accrual | 0.01 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | false | false |
| 01 February 2024 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false |
| 15 February 2024 | Accrual | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false | false |
And Loan Charges tab has the following data:
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
| NSF fee | true | Specified due date | 15 February 2024 | Flat | 10.0 | 0.0 | 0.0 | 10.0 |
@@ -721,6 +721,18 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
this.penaltyAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(penalityCharges));
}
public void setInterestAccrued(BigDecimal interestAccrued) {
this.interestAccrued = interestAccrued;
}
public void setFeeAccrued(BigDecimal feeAccrued) {
this.feeAccrued = feeAccrued;
}
public void setPenaltyAccrued(BigDecimal penaltyAccrued) {
this.penaltyAccrued = penaltyAccrued;
}
public void updateObligationsMet(final MonetaryCurrency currency, final LocalDate transactionDate) {
if (!this.obligationsMet && getTotalOutstanding(currency).isZero()) {
this.obligationsMet = true;
@@ -0,0 +1,32 @@
/**
* 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.portfolio.loanaccount.data;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AccrualBalances {
private BigDecimal interestPortion = BigDecimal.ZERO;
private BigDecimal feePortion = BigDecimal.ZERO;
private BigDecimal penaltyPortion = BigDecimal.ZERO;
}
@@ -56,6 +56,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.AccrualBalances;
import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData;
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData;
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodsData;
@@ -698,13 +699,52 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment();
LocalDate lastDueDate = lastInstallment.getDueDate();
if (isProgressiveAccrual(loan)) {
AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, lastDueDate, true);
for (AccrualPeriodData period : accrualPeriods.getPeriods()) {
Money interestAccrued = period.getTransactionAccrued();
Money feeAccrued = period.getFeeTransactionAccrued();
Money penaltyAccrued = period.getPenaltyTransactionAccrued();
LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(period.getInstallmentNumber());
installment.updateAccrualPortion(interestAccrued, feeAccrued, penaltyAccrued);
AccrualBalances accrualBalances = new AccrualBalances();
accrualTransactions.forEach(lt -> {
switch (lt.getTypeOf()) {
case ACCRUAL -> {
accrualBalances.setFeePortion(MathUtil.add(accrualBalances.getFeePortion(), lt.getFeeChargesPortion()));
accrualBalances.setPenaltyPortion(MathUtil.add(accrualBalances.getPenaltyPortion(), lt.getPenaltyChargesPortion()));
accrualBalances.setInterestPortion(MathUtil.add(accrualBalances.getInterestPortion(), lt.getInterestPortion()));
}
case ACCRUAL_ADJUSTMENT -> {
accrualBalances.setFeePortion(MathUtil.subtract(accrualBalances.getFeePortion(), lt.getFeeChargesPortion()));
accrualBalances
.setPenaltyPortion(MathUtil.subtract(accrualBalances.getPenaltyPortion(), lt.getPenaltyChargesPortion()));
accrualBalances
.setInterestPortion(MathUtil.subtract(accrualBalances.getInterestPortion(), lt.getInterestPortion()));
}
default -> throw new IllegalStateException("Unexpected value: " + lt.getTypeOf());
}
});
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
BigDecimal maximumAccruableInterest = MathUtil.nullToZero(installment.getInterestCharged());
BigDecimal maximumAccruableFee = MathUtil.nullToZero(installment.getFeeChargesCharged());
BigDecimal maximumAccruablePenalty = MathUtil.nullToZero(installment.getPenaltyCharges());
if (MathUtil.isLessThanOrEqualTo(maximumAccruableInterest, accrualBalances.getInterestPortion())) {
installment.setInterestAccrued(maximumAccruableInterest);
accrualBalances.setInterestPortion(accrualBalances.getInterestPortion().subtract(maximumAccruableInterest));
} else {
installment.setInterestAccrued(accrualBalances.getInterestPortion());
accrualBalances.setInterestPortion(BigDecimal.ZERO);
}
if (MathUtil.isLessThanOrEqualTo(maximumAccruableFee, accrualBalances.getFeePortion())) {
installment.setFeeAccrued(maximumAccruableFee);
accrualBalances.setFeePortion(accrualBalances.getFeePortion().subtract(maximumAccruableFee));
} else {
installment.setFeeAccrued(accrualBalances.getFeePortion());
accrualBalances.setFeePortion(BigDecimal.ZERO);
}
if (MathUtil.isLessThanOrEqualTo(maximumAccruablePenalty, accrualBalances.getPenaltyPortion())) {
installment.setPenaltyAccrued(maximumAccruablePenalty);
accrualBalances.setPenaltyPortion(accrualBalances.getPenaltyPortion().subtract(maximumAccruablePenalty));
} else {
installment.setPenaltyAccrued(accrualBalances.getPenaltyPortion());
accrualBalances.setPenaltyPortion(BigDecimal.ZERO);
}
}
} else {
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();