FINERACT-1981: Introduce Interest should not be calculated on past due principal amount

This commit is contained in:
Janos Meszaros
2024-11-29 11:59:18 +01:00
committed by Adam Saghy
parent 3858611190
commit 4cb849fa17
17 changed files with 287 additions and 85 deletions
@@ -36,8 +36,11 @@ public class LoanInterestRecalculationCOBBusinessStep implements LoanCOBBusiness
@Override
public Loan execute(Loan loan) {
if (!loan.isInterestBearing() || !loan.getStatus().isActive() || loan.isNpa() || loan.isChargedOff()
|| !loan.isInterestRecalculationEnabledForProduct()) {
log.debug("Skip processing loan interest recalculation [{}] - reason: not interest bearing loan or not active.", loan.getId());
|| !loan.isInterestRecalculationEnabledForProduct()
|| loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
log.debug(
"Skip processing loan interest recalculation [{}] - Possible reasons: Loan is not an interest bearing loan, Loan is not active, Interest recalculation on past due is disabled on this loan",
loan.getId());
return loan;
}
@@ -694,6 +694,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
+ " lir.compounding_frequency_on_day as compoundingFrequencyOnDay, "
+ " lir.is_compounding_to_be_posted_as_transaction as isCompoundingToBePostedAsTransaction, "
+ " lir.allow_compounding_on_eod as allowCompoundingOnEod, "
+ " lir.disallow_interest_calc_on_past_due as disallowInterestCalculationOnPastDue, "
+ " l.is_floating_interest_rate as isFloatingInterestRate, "
+ " l.interest_rate_differential as interestRateDifferential, "
+ " l.create_standing_instruction_at_disbursement as createStandingInstructionAtDisbursement, "
@@ -1037,11 +1038,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
final Boolean isCompoundingToBePostedAsTransaction = rs.getBoolean("isCompoundingToBePostedAsTransaction");
final Boolean allowCompoundingOnEod = rs.getBoolean("allowCompoundingOnEod");
final Boolean disallowInterestCalculationOnPastDue = rs.getBoolean("disallowInterestCalculationOnPastDue");
interestRecalculationData = new LoanInterestRecalculationData(lprId, productId, interestRecalculationCompoundingType,
rescheduleStrategyType, calendarData, restFrequencyType, restFrequencyInterval, restFrequencyNthDayEnum,
restFrequencyWeekDayEnum, restFrequencyOnDay, compoundingCalendarData, compoundingFrequencyType,
compoundingInterval, compoundingFrequencyNthDayEnum, compoundingFrequencyWeekDayEnum, compoundingFrequencyOnDay,
isCompoundingToBePostedAsTransaction, allowCompoundingOnEod);
isCompoundingToBePostedAsTransaction, allowCompoundingOnEod, disallowInterestCalculationOnPastDue);
}
final boolean canUseForTopup = rs.getBoolean("canUseForTopup");
@@ -2039,42 +2041,49 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
@Override
public Collection<Long> fetchLoansForInterestRecalculation() {
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT ml.id FROM m_loan ml ");
sqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
sqlBuilder.append(" LEFT JOIN m_loan_disbursement_detail dd on dd.loan_id=ml.id and dd.disbursedon_date is null ");
// For Floating rate changes
sqlBuilder.append(
" left join m_product_loan_floating_rates pfr on ml.product_id = pfr.loan_product_id and ml.is_floating_interest_rate = true");
sqlBuilder.append(" left join m_floating_rates fr on pfr.floating_rates_id = fr.id");
sqlBuilder.append(" left join m_floating_rates_periods frp on fr.id = frp.floating_rates_id ");
sqlBuilder.append(" left join m_loan_reschedule_request lrr on lrr.loan_id = ml.id");
// this is to identify the applicable rates when base rate is changed
sqlBuilder.append(" left join m_floating_rates bfr on bfr.is_base_lending_rate = true");
sqlBuilder.append(" left join m_floating_rates_periods bfrp on bfr.id = bfrp.floating_rates_id and bfrp.created_date >= ?");
sqlBuilder.append(" WHERE ml.loan_status_id = ? ");
sqlBuilder.append(" and ml.is_npa = false and ml.is_charged_off = false and dd.is_reversed = false ");
sqlBuilder.append(" and ((");
sqlBuilder.append("ml.interest_recalculation_enabled = true ");
sqlBuilder.append(" and (ml.interest_recalcualated_on is null or ml.interest_recalcualated_on <> ?)");
sqlBuilder.append(" and ((");
sqlBuilder.append(" mr.completed_derived is false ");
sqlBuilder.append(" and mr.duedate < ? )");
sqlBuilder.append(" or dd.expected_disburse_date < ? )) ");
sqlBuilder.append(" or (");
sqlBuilder.append(" fr.is_active = true and frp.is_active = true");
sqlBuilder.append(" and (frp.created_date >= ? or ");
sqlBuilder
.append("(bfrp.id is not null and frp.is_differential_to_base_lending_rate = true and frp.from_date >= bfrp.from_date)) ");
sqlBuilder.append("and lrr.loan_id is null");
sqlBuilder.append(" ))");
sqlBuilder.append(" group by ml.id");
final String sql = """
SELECT l.id
FROM m_loan l
INNER JOIN m_loan_repayment_schedule mr ON mr.loan_id = l.id
LEFT JOIN m_loan_disbursement_detail dd ON dd.loan_id=l.id AND dd.disbursedon_date IS NULL
-- for past due interest recalculation
LEFT JOIN m_loan_recalculation_details rcd ON rcd.loan_id = l.id
-- For Floating rate changes
LEFT JOIN m_product_loan_floating_rates pfr
ON l.product_id = pfr.loan_product_id AND l.is_floating_interest_rate = TRUE
LEFT JOIN m_floating_rates fr ON pfr.floating_rates_id = fr.id
LEFT JOIN m_floating_rates_periods frp ON fr.id = frp.floating_rates_id
LEFT JOIN m_loan_reschedule_request lrr ON lrr.loan_id = l.id
-- this is to identify the applicable rates when base rate is changed
LEFT JOIN m_floating_rates bfr ON bfr.is_base_lending_rate = TRUE
LEFT JOIN m_floating_rates_periods bfrp ON bfr.id = bfrp.floating_rates_id AND bfrp.created_date >= ?
WHERE l.loan_status_id = ?
AND l.is_npa = FALSE
AND l.is_charged_off = FALSE
AND dd.is_reversed = FALSE
AND (
(l.interest_recalculation_enabled = TRUE
AND (l.interest_recalcualated_on IS NULL OR l.interest_recalcualated_on <> ?)
AND ((mr.completed_derived IS FALSE AND mr.duedate < ?) OR dd.expected_disburse_date < ?)
AND rcd.disallow_interest_calc_on_past_due = FALSE)
OR
-- float rate changes
(fr.is_active = TRUE
AND frp.is_active = TRUE
AND (frp.created_date >= ?
OR (bfrp.id IS NOT NULL
AND frp.is_differential_to_base_lending_rate = TRUE
AND frp.from_date >= bfrp.from_date))
AND lrr.loan_id IS NULL)
)
GROUP BY l.id
""";
try {
LocalDate currentdate = DateUtils.getBusinessLocalDate();
// will look only for yesterday modified rates
LocalDate yesterday = DateUtils.getBusinessLocalDate().minusDays(1);
return this.jdbcTemplate.queryForList(sqlBuilder.toString(), Long.class, yesterday, LoanStatus.ACTIVE.getValue(), currentdate,
currentdate, currentdate, yesterday);
return this.jdbcTemplate.queryForList(sql, Long.class, yesterday, LoanStatus.ACTIVE.getValue(), currentdate, currentdate,
currentdate, yesterday);
} catch (final EmptyResultDataAccessException e) {
return null;
}
@@ -2085,45 +2094,50 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
LocalDate currentdate = DateUtils.getBusinessLocalDate();
// will look only for yesterday modified rates
LocalDate yesterday = DateUtils.getBusinessLocalDate().minusDays(1);
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("SELECT ml.id FROM m_loan ml ");
sqlBuilder.append(" left join m_client mc on mc.id = ml.client_id ");
sqlBuilder.append(" left join m_office o on mc.office_id = o.id ");
sqlBuilder.append(" INNER JOIN m_loan_repayment_schedule mr on mr.loan_id = ml.id ");
sqlBuilder.append(
" LEFT JOIN m_loan_disbursement_detail dd on dd.loan_id=ml.id and dd.disbursedon_date is null and dd.is_reversed = false ");
// For Floating rate changes
sqlBuilder.append(
" left join m_product_loan_floating_rates pfr on ml.product_id = pfr.loan_product_id and ml.is_floating_interest_rate = true");
sqlBuilder.append(" left join m_floating_rates fr on pfr.floating_rates_id = fr.id");
sqlBuilder.append(" left join m_floating_rates_periods frp on fr.id = frp.floating_rates_id ");
sqlBuilder.append(" left join m_loan_reschedule_request lrr on lrr.loan_id = ml.id");
// this is to identify the applicable rates when base rate is changed
sqlBuilder.append(" left join m_floating_rates bfr on bfr.is_base_lending_rate = true");
sqlBuilder.append(" left join m_floating_rates_periods bfrp on bfr.id = bfrp.floating_rates_id and bfrp.created_date >= ?");
sqlBuilder.append(" WHERE ml.loan_status_id = ? ");
sqlBuilder.append(" and ml.is_npa = false and ml.is_charged_off = false ");
sqlBuilder.append(" and ((");
sqlBuilder.append("ml.interest_recalculation_enabled = true ");
sqlBuilder.append(" and (ml.interest_recalcualated_on is null or ml.interest_recalcualated_on <> ? )");
sqlBuilder.append(" and ((");
sqlBuilder.append(" mr.completed_derived is false ");
sqlBuilder.append(" and mr.duedate < ? )");
sqlBuilder.append(" or dd.expected_disburse_date < ? )) ");
sqlBuilder.append(" or (");
sqlBuilder.append(" fr.is_active = true and frp.is_active = true");
sqlBuilder.append(" and (frp.created_date >= ? or ");
sqlBuilder
.append("(bfrp.id is not null and frp.is_differential_to_base_lending_rate = true and frp.from_date >= bfrp.from_date)) ");
sqlBuilder.append("and lrr.loan_id is null");
sqlBuilder.append(" ))");
sqlBuilder.append(" and ml.id >= ? and o.hierarchy like ? ");
sqlBuilder.append(" group by ml.id ");
sqlBuilder.append(" limit ? ");
final String sql = """
SELECT l.id
FROM m_loan l
LEFT JOIN m_client c ON c.id = l.client_id
LEFT JOIN m_office o ON c.office_id = o.id
INNER JOIN m_loan_repayment_schedule rps ON rps.loan_id = l.id
LEFT JOIN m_loan_disbursement_detail dd
ON dd.loan_id=l.id AND dd.disbursedon_date IS NULL AND dd.is_reversed = FALSE
-- for past due interest recalculation
LEFT JOIN m_loan_recalculation_details rcd ON rcd.loan_id = l.id
-- For Floating rate changes
LEFT JOIN m_product_loan_floating_rates pfr
ON l.product_id = pfr.loan_product_id AND l.is_floating_interest_rate = TRUE
LEFT JOIN m_floating_rates fr ON pfr.floating_rates_id = fr.id
LEFT JOIN m_floating_rates_periods frp ON fr.id = frp.floating_rates_id
LEFT JOIN m_loan_reschedule_request lrr ON lrr.loan_id = l.id
-- this is to identify the applicable rates when base rate is changed
LEFT JOIN m_floating_rates bfr ON bfr.is_base_lending_rate = TRUE
LEFT JOIN m_floating_rates_periods bfrp ON bfr.id = bfrp.floating_rates_id AND bfrp.created_date >= ?
WHERE l.loan_status_id = ?
AND l.is_npa = FALSE
AND l.is_charged_off = FALSE
AND (
(l.interest_recalculation_enabled = TRUE
AND (l.interest_recalcualated_on IS NULL OR l.interest_recalcualated_on <> ?)
AND ((rps.completed_derived IS FALSE AND rps.duedate < ?) OR dd.expected_disburse_date < ?)
AND rcd.disallow_interest_calc_on_past_due = FALSE)
OR
(fr.is_active = TRUE
AND frp.is_active = TRUE
AND (frp.created_date >= ?
OR (bfrp.id IS NOT NULL
AND frp.is_differential_to_base_lending_rate = TRUE
AND frp.from_date >= bfrp.from_date))
AND lrr.loan_id IS NULL)
)
AND l.id >= ?
AND o.hierarchy like ?
GROUP BY l.id
LIMIT ?
""";
try {
return Collections.synchronizedList(
this.jdbcTemplate.queryForList(sqlBuilder.toString(), Long.class, yesterday, LoanStatus.ACTIVE.getValue(), currentdate,
currentdate, currentdate, yesterday, maxLoanIdInList, officeHierarchy, pageSize));
return Collections.synchronizedList(this.jdbcTemplate.queryForList(sql, Long.class, yesterday, LoanStatus.ACTIVE.getValue(),
currentdate, currentdate, currentdate, yesterday, maxLoanIdInList, officeHierarchy, pageSize));
} catch (final EmptyResultDataAccessException e) {
return null;
}
@@ -174,7 +174,8 @@ public final class LoanProductDataValidator {
LoanProductConstants.recalculationRestFrequencyWeekdayParamName, LoanProductConstants.recalculationRestFrequencyNthDayParamName,
LoanProductConstants.recalculationRestFrequencyOnDayParamName,
LoanProductConstants.isCompoundingToBePostedAsTransactionParamName, LoanProductConstants.allowCompoundingOnEodParamName,
LoanProductConstants.CAN_USE_FOR_TOPUP, LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME,
LoanProductConstants.disallowInterestCalculationOnPastDueParamName, LoanProductConstants.CAN_USE_FOR_TOPUP,
LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, LoanProductConstants.RATES_PARAM_NAME,
LoanProductConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DISALLOW_EXPECTED_DISBURSEMENTS,
LoanProductConstants.ALLOW_APPROVED_DISBURSED_AMOUNTS_OVER_APPLIED, LoanProductConstants.OVER_APPLIED_CALCULATION_TYPE,
LoanProductConstants.OVER_APPLIED_NUMBER, LoanProductConstants.DELINQUENCY_BUCKET_PARAM_NAME,
@@ -1070,6 +1071,12 @@ public final class LoanProductDataValidator {
if (fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_TYPE, element)) {
loanScheduleType = fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element);
}
if (LoanScheduleType.CUMULATIVE.equals(LoanScheduleType.valueOf(loanScheduleType))
&& fromApiJsonHelper.parameterExists(LoanProductConstants.disallowInterestCalculationOnPastDueParamName, element)) {
baseDataValidator.reset().parameter(LoanProductConstants.disallowInterestCalculationOnPastDueParamName).failWithCode(
"disallow.interest.calculation.on.past.due.not.supported.for.loan.schedule.type.cumulative",
"Do not calculate interest on past due principal balances is only supported for Progressive loan schedule type");
}
if (LoanScheduleType.CUMULATIVE.equals(LoanScheduleType.valueOf(loanScheduleType))
&& LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.equals(loanRescheduleStrategyMethod)) {
baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).failWithCode(
@@ -259,7 +259,9 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
+ "lpr.compounding_frequency_weekday_enum as compoundingFrequencyWeekDayEnum, "
+ "lpr.compounding_frequency_on_day as compoundingFrequencyOnDay, "
+ "lpr.is_compounding_to_be_posted_as_transaction as isCompoundingToBePostedAsTransaction, "
+ "lpr.allow_compounding_on_eod as allowCompoundingOnEod, " + "lp.hold_guarantee_funds as holdGuaranteeFunds, "
+ "lpr.allow_compounding_on_eod as allowCompoundingOnEod, "
+ "lpr.disallow_interest_calc_on_past_due as disallowInterestCalculationOnPastDue, "
+ "lp.hold_guarantee_funds as holdGuaranteeFunds, "
+ "lp.principal_threshold_for_last_installment as principalThresholdForLastInstallment, "
+ "lp.fixed_principal_percentage_per_installment fixedPrincipalPercentagePerInstallment, "
+ "lp.sync_expected_with_disbursement_date as syncExpectedWithDisbursementDate, "
@@ -477,12 +479,14 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo
final EnumOptionData preCloseInterestCalculationStrategy = LoanEnumerations
.preCloseInterestCalculationStrategy(preCloseInterestCalculationStrategyEnumValue);
final boolean allowCompoundingOnEod = rs.getBoolean("allowCompoundingOnEod");
final boolean disallowInterestCalculationOnPastDue = rs.getBoolean("disallowInterestCalculationOnPastDue");
interestRecalculationData = new LoanProductInterestRecalculationData(lprId, productId, interestRecalculationCompoundingType,
rescheduleStrategyType, restFrequencyType, restFrequencyInterval, restFrequencyNthDayEnum, restFrequencyWeekDayEnum,
restFrequencyOnDay, compoundingFrequencyType, compoundingInterval, compoundingFrequencyNthDayEnum,
compoundingFrequencyWeekDayEnum, compoundingFrequencyOnDay, isArrearsBasedOnOriginalSchedule,
isCompoundingToBePostedAsTransaction, preCloseInterestCalculationStrategy, allowCompoundingOnEod);
isCompoundingToBePostedAsTransaction, preCloseInterestCalculationStrategy, allowCompoundingOnEod,
disallowInterestCalculationOnPastDue);
}
final boolean amortization = rs.getBoolean("amortizationBoolean");
@@ -174,4 +174,5 @@
<include file="parts/0153_add_charge_off_reason_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" />
<include file="parts/0154_add_interest_refund_to_r_enum_value.xml" relativeToChangelogFile="true" />
<include file="parts/0155_add_configuration_enable_immediate_charge_accrual_post_maturity.xml" relativeToChangelogFile="true" />
<include file="parts/0156_add_disallow_interest_calc_on_past_due_field.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet id="1" author="fineract">
<addColumn tableName="m_product_loan_recalculation_details">
<column name="disallow_interest_calc_on_past_due" defaultValueBoolean="false" type="boolean">
<constraints nullable="false"/>
</column>
</addColumn>
<addColumn tableName="m_loan_recalculation_details">
<column name="disallow_interest_calc_on_past_due" defaultValueBoolean="false" type="boolean">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>