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
@@ -146,6 +146,14 @@
"null",
"boolean"
]
},
{
"default": null,
"name": "disallowInterestCalculationOnPastDue",
"type": [
"null",
"boolean"
]
}
]
}
@@ -146,6 +146,14 @@
"null",
"boolean"
]
},
{
"default": null,
"name": "disallowInterestCalculationOnPastDue",
"type": [
"null",
"boolean"
]
}
]
}
@@ -47,6 +47,7 @@ public class LoanInterestRecalculationData {
private Boolean isCompoundingToBePostedAsTransaction;
private CalendarData compoundingCalendarData;
private Boolean allowCompoundingOnEod;
private Boolean disallowInterestCalculationOnPastDue;
public LoanInterestRecalculationData(final Long id, final Long loanId, final EnumOptionData interestRecalculationCompoundingType,
final EnumOptionData rescheduleStrategyType, final CalendarData calendarData,
@@ -56,7 +57,7 @@ public class LoanInterestRecalculationData {
final EnumOptionData recalculationCompoundingFrequencyType, final Integer recalculationCompoundingFrequencyInterval,
final EnumOptionData recalculationCompoundingFrequencyNthDay, final EnumOptionData recalculationCompoundingFrequencyWeekday,
final Integer recalculationCompoundingFrequencyOnDay, final Boolean isCompoundingToBePostedAsTransaction,
final Boolean allowCompoundingOnEod) {
final Boolean allowCompoundingOnEod, final Boolean disallowInterestCalculationOnPastDue) {
this.id = id;
this.loanId = loanId;
this.interestRecalculationCompoundingType = interestRecalculationCompoundingType;
@@ -75,6 +76,7 @@ public class LoanInterestRecalculationData {
this.compoundingCalendarData = compoundingCalendarData;
this.isCompoundingToBePostedAsTransaction = isCompoundingToBePostedAsTransaction;
this.allowCompoundingOnEod = allowCompoundingOnEod;
this.disallowInterestCalculationOnPastDue = disallowInterestCalculationOnPastDue;
}
public LoanInterestRecalculationData withCalendarData(final CalendarData calendarData, CalendarData compoundingCalendarData) {
@@ -88,6 +88,9 @@ public class LoanInterestRecalculationDetails extends AbstractPersistableCustom<
@Column(name = "allow_compounding_on_eod")
private Boolean allowCompoundingOnEod;
@Column(name = "disallow_interest_calc_on_past_due")
private Boolean disallowInterestCalculationOnPastDue;
protected LoanInterestRecalculationDetails() {
// Default constructor for jpa repository
}
@@ -96,7 +99,7 @@ public class LoanInterestRecalculationDetails extends AbstractPersistableCustom<
final Integer restFrequencyType, final Integer restInterval, final Integer restFrequencyNthDay, Integer restFrequencyWeekday,
Integer restFrequencyOnDay, Integer compoundingFrequencyType, Integer compoundingInterval, Integer compoundingFrequencyNthDay,
Integer compoundingFrequencyWeekday, Integer compoundingFrequencyOnDay, final boolean isCompoundingToBePostedAsTransaction,
final boolean allowCompoundingOnEod) {
final boolean allowCompoundingOnEod, final boolean disallowInterestCalculationOnPastDue) {
this.interestRecalculationCompoundingMethod = interestRecalculationCompoundingMethod;
this.rescheduleStrategyMethod = rescheduleStrategyMethod;
this.restFrequencyNthDay = restFrequencyNthDay;
@@ -111,6 +114,7 @@ public class LoanInterestRecalculationDetails extends AbstractPersistableCustom<
this.compoundingInterval = compoundingInterval;
this.isCompoundingToBePostedAsTransaction = isCompoundingToBePostedAsTransaction;
this.allowCompoundingOnEod = allowCompoundingOnEod;
this.disallowInterestCalculationOnPastDue = disallowInterestCalculationOnPastDue;
}
public static LoanInterestRecalculationDetails createFrom(
@@ -127,7 +131,8 @@ public class LoanInterestRecalculationDetails extends AbstractPersistableCustom<
loanProductInterestRecalculationDetails.getCompoundingFrequencyWeekday(),
loanProductInterestRecalculationDetails.getCompoundingFrequencyOnDay(),
loanProductInterestRecalculationDetails.getIsCompoundingToBePostedAsTransaction(),
loanProductInterestRecalculationDetails.allowCompoundingOnEod());
loanProductInterestRecalculationDetails.allowCompoundingOnEod(),
loanProductInterestRecalculationDetails.disallowInterestCalculationOnPastDue());
}
public void updateLoan(final Loan loan) {
@@ -189,4 +194,8 @@ public class LoanInterestRecalculationDetails extends AbstractPersistableCustom<
public boolean allowCompoundingOnEod() {
return this.allowCompoundingOnEod;
}
public Boolean disallowInterestCalculationOnPastDue() {
return disallowInterestCalculationOnPastDue;
}
}
@@ -97,6 +97,7 @@ public interface LoanProductConstants {
String recalculationCompoundingFrequencyNthDayParamName = "recalculationCompoundingFrequencyNthDayType";
String recalculationCompoundingFrequencyOnDayParamName = "recalculationCompoundingFrequencyOnDayType";
String isCompoundingToBePostedAsTransactionParamName = "isCompoundingToBePostedAsTransaction";
String disallowInterestCalculationOnPastDueParamName = "disallowInterestCalculationOnPastDue";
// Guarantee related
String holdGuaranteeFundsParamName = "holdGuaranteeFunds";
@@ -198,6 +198,8 @@ final class LoanProductsApiResourceSwagger {
public Boolean isCompoundingToBePostedAsTransaction;
@Schema(example = "false")
public Boolean allowCompoundingOnEod;
@Schema(example = "false")
public Boolean disallowInterestCalculationOnPastDue;
// Accounting
@Schema(example = "3")
@@ -520,6 +522,8 @@ final class LoanProductsApiResourceSwagger {
public GetLoanProductsPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy;
@Schema(example = "true")
public Boolean isArrearsBasedOnOriginalSchedule;
@Schema(example = "false")
public Boolean disallowInterestCalculationOnPastDue;
@Schema(example = "true")
public Boolean isCompoundingToBePostedAsTransaction;
@Schema(example = "true")
@@ -1182,7 +1182,7 @@ public class LoanProductData implements Serializable {
getInterestRecalculationRestOnDayType(), compoundingCalendarData, getRecalculationCompoundingFrequencyType(),
getRecalculationCompoundingFrequencyInterval(), getInterestRecalculationCompoundingNthDayType(),
getInterestRecalculationCompoundingWeekDayType(), getInterestRecalculationCompoundingOnDayType(),
isCompoundingToBePostedAsTransaction(), allowCompoundingOnEod());
isCompoundingToBePostedAsTransaction(), allowCompoundingOnEod(), disallowInterestCalculationOnPastDue());
}
private EnumOptionData getRescheduleStrategyType() {
@@ -1279,6 +1279,11 @@ public class LoanProductData implements Serializable {
return isInterestRecalculationEnabled() ? this.interestRecalculationData.isAllowCompoundingOnEod() : null;
}
@SuppressFBWarnings("NP_BOOLEAN_RETURN_NULL")
public Boolean disallowInterestCalculationOnPastDue() {
return isInterestRecalculationEnabled() ? this.interestRecalculationData.disallowInterestCalculationOnPastDue() : null;
}
public void setLoanProductConfigurableAttributes(LoanProductConfigurableAttributes loanProductConfigurableAttributes) {
this.allowAttributeOverrides = loanProductConfigurableAttributes;
}
@@ -50,6 +50,7 @@ public class LoanProductInterestRecalculationData implements Serializable {
private final boolean isCompoundingToBePostedAsTransaction;
private final EnumOptionData preClosureInterestCalculationStrategy;
private final boolean allowCompoundingOnEod;
private final Boolean disallowInterestCalculationOnPastDue;
public LoanProductInterestRecalculationData(final Long id, final Long productId,
final EnumOptionData interestRecalculationCompoundingType, final EnumOptionData rescheduleStrategyType,
@@ -59,7 +60,8 @@ public class LoanProductInterestRecalculationData implements Serializable {
final Integer recalculationCompoundingFrequencyInterval, final EnumOptionData recalculationCompoundingFrequencyNthDay,
final EnumOptionData recalculationCompoundingFrequencyWeekday, final Integer recalculationCompoundingFrequencyOnDay,
final boolean isArrearsBasedOnOriginalSchedule, boolean isCompoundingToBePostedAsTransaction,
final EnumOptionData preCloseInterestCalculationStrategy, final boolean allowCompoundingOnEod) {
final EnumOptionData preCloseInterestCalculationStrategy, final boolean allowCompoundingOnEod,
final Boolean disallowInterestCalculationOnPastDue) {
this.id = id;
this.productId = productId;
this.interestRecalculationCompoundingType = interestRecalculationCompoundingType;
@@ -78,6 +80,7 @@ public class LoanProductInterestRecalculationData implements Serializable {
this.preClosureInterestCalculationStrategy = preCloseInterestCalculationStrategy;
this.isCompoundingToBePostedAsTransaction = isCompoundingToBePostedAsTransaction;
this.allowCompoundingOnEod = allowCompoundingOnEod;
this.disallowInterestCalculationOnPastDue = disallowInterestCalculationOnPastDue;
}
public static LoanProductInterestRecalculationData sensibleDefaultsForNewLoanProductCreation() {
@@ -101,12 +104,15 @@ public class LoanProductInterestRecalculationData implements Serializable {
final EnumOptionData preCloseInterestCalculationStrategy = preCloseInterestCalculationStrategy(
LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE);
final boolean allowCompoundingOnEod = false;
final boolean disallowInterestCalculationOnPastDue = false;
return new LoanProductInterestRecalculationData(id, productId, interestRecalculationCompoundingType, rescheduleStrategyType,
recalculationRestFrequencyType, recalculationRestFrequencyInterval, recalculationRestFrequencyNthDay,
recalculationRestFrequencyWeekday, recalculationRestFrequencyOnDay, recalculationCompoundingFrequencyType,
recalculationCompoundingFrequencyInterval, recalculationCompoundingFrequencyNthDay,
recalculationCompoundingFrequencyWeekday, recalculationCompoundingFrequencyOnDay, isArrearsBasedOnOriginalSchedule,
isCompoundingToBePostedAsTransaction, preCloseInterestCalculationStrategy, allowCompoundingOnEod);
isCompoundingToBePostedAsTransaction, preCloseInterestCalculationStrategy, allowCompoundingOnEod,
disallowInterestCalculationOnPastDue);
}
public boolean isArrearsBasedOnOriginalSchedule() {
@@ -124,4 +130,8 @@ public class LoanProductInterestRecalculationData implements Serializable {
public boolean isIsCompoundingToBePostedAsTransaction() {
return isCompoundingToBePostedAsTransaction;
}
public Boolean disallowInterestCalculationOnPastDue() {
return disallowInterestCalculationOnPastDue;
}
}
@@ -96,6 +96,9 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
@Column(name = "allow_compounding_on_eod")
private Boolean allowCompoundingOnEod;
@Column(name = "disallow_interest_calc_on_past_due")
private Boolean disallowInterestCalculationOnPastDue;
protected LoanProductInterestRecalculationDetails() {
//
}
@@ -162,11 +165,14 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
final boolean isCompoundingToBePostedAsTransaction = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.isCompoundingToBePostedAsTransactionParamName);
final boolean disallowInterestCalculationOnPastDue = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.disallowInterestCalculationOnPastDueParamName);
return new LoanProductInterestRecalculationDetails(interestRecalculationCompoundingMethod, loanRescheduleStrategyMethod,
recurrenceFrequency, recurrenceInterval, recurrenceOnNthDay, recurrenceOnDay, recurrenceOnWeekday,
compoundingRecurrenceFrequency, compoundingInterval, compoundingRecurrenceOnNthDay, compoundingRecurrenceOnDay,
compoundingRecurrenceOnWeekday, isArrearsBasedOnOriginalSchedule, preCloseInterestCalculationStrategy,
isCompoundingToBePostedAsTransaction, allowCompoundingOnEod);
isCompoundingToBePostedAsTransaction, allowCompoundingOnEod, disallowInterestCalculationOnPastDue);
}
private LoanProductInterestRecalculationDetails(final Integer interestRecalculationCompoundingMethod,
@@ -175,7 +181,8 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
Integer compoundingFrequencyType, Integer compoundingInterval, final Integer compoundingFrequencyNthDay,
final Integer compoundingFrequencyOnDay, final Integer compoundingFrequencyWeekday,
final boolean isArrearsBasedOnOriginalSchedule, final Integer preCloseInterestCalculationStrategy,
final boolean isCompoundingToBePostedAsTransaction, final boolean allowCompoundingOnEod) {
final boolean isCompoundingToBePostedAsTransaction, final boolean allowCompoundingOnEod,
final boolean disallowInterestCalculationOnPastDue) {
this.interestRecalculationCompoundingMethod = interestRecalculationCompoundingMethod;
this.rescheduleStrategyMethod = rescheduleStrategyMethod;
this.restFrequencyType = restFrequencyType;
@@ -192,6 +199,7 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
this.preClosureInterestCalculationStrategy = preCloseInterestCalculationStrategy;
this.isCompoundingToBePostedAsTransaction = isCompoundingToBePostedAsTransaction;
this.allowCompoundingOnEod = allowCompoundingOnEod;
this.disallowInterestCalculationOnPastDue = disallowInterestCalculationOnPastDue;
}
public void updateProduct(final LoanProduct loanProduct) {
@@ -406,6 +414,13 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
this.isCompoundingToBePostedAsTransaction = newValue;
}
if (command.isChangeInBooleanParameterNamed(LoanProductConstants.disallowInterestCalculationOnPastDueParamName,
this.disallowInterestCalculationOnPastDue)) {
final boolean newValue = command
.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.disallowInterestCalculationOnPastDueParamName);
actualChanges.put(LoanProductConstants.disallowInterestCalculationOnPastDueParamName, newValue);
this.disallowInterestCalculationOnPastDue = newValue;
}
}
public RecalculationFrequencyType getRestFrequencyType() {
@@ -463,4 +478,8 @@ public class LoanProductInterestRecalculationDetails extends AbstractPersistable
public Boolean allowCompoundingOnEod() {
return this.allowCompoundingOnEod;
}
public Boolean disallowInterestCalculationOnPastDue() {
return disallowInterestCalculationOnPastDue;
}
}
@@ -982,6 +982,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, LoanRepaymentScheduleInstallment currentInstallment,
Money overduePrincipal, Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
return;
}
LocalDate fromDate = currentInstallment.getFromDate();
LocalDate toDate = currentInstallment.getDueDate();
boolean hasUpdate = false;
@@ -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>
@@ -69,7 +69,7 @@ public class LoanInterestRecalculationCOBTest extends BaseLoanIntegrationTest {
Utils.initializeRESTAssured();
requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
requestSpec.header("Fineract-Platform-TenantId", "default");
requestSpec.header("Fineract-Platform-TenantId", Utils.DEFAULT_TENANT);
responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
schedulerJobHelper = new SchedulerJobHelper(requestSpec);
@@ -1222,4 +1222,68 @@ public class LoanInterestRecalculationCOBTest extends BaseLoanIntegrationTest {
payoffOnDateAndVerifyStatus("1 May 2023", loanIdRef.get());
}
@Test
public void verifyEarlyLateRepaymentOnProgressiveLoanThePastDueHasNoEffect() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2024", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper //
.createLoanProduct(create4IProgressive() //
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
.disallowInterestCalculationOnPastDue(true) //
);
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2024", 100.0, 7.0, 6,
null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "01 February 2024", 16.43, 0.0, 0.0, 0.58);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 16.52, 0.0, 0.0, 0.49);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 16.62, 0.0, 0.0, 0.39);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 16.72, 0.0, 0.0, 0.29);
validateFullyUnpaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 16.81, 0.0, 0.0, 0.20);
validateFullyUnpaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 16.90, 0.0, 0.0, 0.10);
});
runAt("15 February 2024", () -> { // we have past due and should not count extra interest
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
loanTransactionHelper.makeLoanRepayment("15 February 2024", 15.0F, loanId.intValue());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.43, 15.0, 1.43, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.58, 0,
0.58, 0.0, 15.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "01 March 2024", 16.52, 0.0, 0.0, 0.49);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 16.62, 0.0, 0.0, 0.39);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 16.72, 0.0, 0.0, 0.29);
validateFullyUnpaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 16.81, 0.0, 0.0, 0.20);
validateFullyUnpaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 16.90, 0.0, 0.0, 0.10);
});
runAt("15 February 2024", () -> { // we turn from past due into early repayment and should have less interest
Long loanId = loanIdRef.get();
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
loanTransactionHelper.makeLoanRepayment("15 February 2024", 19.02F, loanId.intValue());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
logLoanDetails(loanDetails);
validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.43, 16.43, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.58, 0.58,
0.0, 0.0, 17.01);
validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 16.77, 16.77, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.24, 0.24,
0.0, 17.01, 0.0);
validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "01 April 2024", 16.42, 0.0, 0.0, 0.59);
validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "01 May 2024", 16.72, 0.0, 0.0, 0.29);
validateFullyUnpaidRepaymentPeriod(loanDetails, 5, "01 June 2024", 16.81, 0.0, 0.0, 0.20);
validateFullyUnpaidRepaymentPeriod(loanDetails, 6, "01 July 2024", 16.85, 0.0, 0.0, 0.10);
});
}
}