From 4012a13becf0c77a84eb5f7aa4c1146ee591ebf9 Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Tue, 26 Nov 2024 18:47:09 +0100 Subject: [PATCH] FINERACT-1981: Introduce Interest should not be calculated on past due principal amount --- .../v1/LoanInterestRecalculationDataV1.avsc | 8 +++ ...oanProductInterestRecalculationDataV1.avsc | 8 +++ .../data/LoanInterestRecalculationData.java | 4 +- .../LoanInterestRecalculationDetails.java | 13 +++- .../loanproduct/LoanProductConstants.java | 1 + .../api/LoanProductsApiResourceSwagger.java | 4 ++ .../loanproduct/data/LoanProductData.java | 7 +- .../LoanProductInterestRecalculationData.java | 14 +++- ...anProductInterestRecalculationDetails.java | 23 ++++++- ...edPaymentScheduleTransactionProcessor.java | 4 ++ .../service/LoanReadPlatformServiceImpl.java | 4 +- .../LoanProductDataValidator.java | 9 ++- .../LoanProductReadPlatformServiceImpl.java | 8 ++- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...sallow_interest_calc_on_past_due_field.xml | 39 +++++++++++ .../LoanInterestRecalculationCOBTest.java | 66 ++++++++++++++++++- 16 files changed, 200 insertions(+), 13 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0155_add_disallow_interest_calc_on_past_due_field.xml diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanInterestRecalculationDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanInterestRecalculationDataV1.avsc index c799b4240f3..29e789ced2f 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanInterestRecalculationDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanInterestRecalculationDataV1.avsc @@ -146,6 +146,14 @@ "null", "boolean" ] + }, + { + "default": null, + "name": "disallowInterestCalculationOnPastDue", + "type": [ + "null", + "boolean" + ] } ] } diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductInterestRecalculationDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductInterestRecalculationDataV1.avsc index 483116378fa..fffb4dce8b3 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductInterestRecalculationDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductInterestRecalculationDataV1.avsc @@ -146,6 +146,14 @@ "null", "boolean" ] + }, + { + "default": null, + "name": "disallowInterestCalculationOnPastDue", + "type": [ + "null", + "boolean" + ] } ] } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanInterestRecalculationData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanInterestRecalculationData.java index 6c5c6aa41c0..60b08374cf2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanInterestRecalculationData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanInterestRecalculationData.java @@ -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 LoanInterestRecalculationData(final Long id, final Long loanId, final Enu 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 LoanInterestRecalculationData(final Long id, final Long loanId, final Enu this.compoundingCalendarData = compoundingCalendarData; this.isCompoundingToBePostedAsTransaction = isCompoundingToBePostedAsTransaction; this.allowCompoundingOnEod = allowCompoundingOnEod; + this.disallowInterestCalculationOnPastDue = disallowInterestCalculationOnPastDue; } public LoanInterestRecalculationData withCalendarData(final CalendarData calendarData, CalendarData compoundingCalendarData) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java index 5da30a483cc..454e524e0bc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInterestRecalculationDetails.java @@ -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 @@ private LoanInterestRecalculationDetails(final Integer interestRecalculationComp 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 @@ private LoanInterestRecalculationDetails(final Integer interestRecalculationComp this.compoundingInterval = compoundingInterval; this.isCompoundingToBePostedAsTransaction = isCompoundingToBePostedAsTransaction; this.allowCompoundingOnEod = allowCompoundingOnEod; + this.disallowInterestCalculationOnPastDue = disallowInterestCalculationOnPastDue; } public static LoanInterestRecalculationDetails createFrom( @@ -127,7 +131,8 @@ public static LoanInterestRecalculationDetails createFrom( loanProductInterestRecalculationDetails.getCompoundingFrequencyWeekday(), loanProductInterestRecalculationDetails.getCompoundingFrequencyOnDay(), loanProductInterestRecalculationDetails.getIsCompoundingToBePostedAsTransaction(), - loanProductInterestRecalculationDetails.allowCompoundingOnEod()); + loanProductInterestRecalculationDetails.allowCompoundingOnEod(), + loanProductInterestRecalculationDetails.disallowInterestCalculationOnPastDue()); } public void updateLoan(final Loan loan) { @@ -189,4 +194,8 @@ public boolean isCompoundingToBePostedAsTransaction() { public boolean allowCompoundingOnEod() { return this.allowCompoundingOnEod; } + + public Boolean disallowInterestCalculationOnPastDue() { + return disallowInterestCalculationOnPastDue; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index 90308e5b385..219e0ac52ee 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -97,6 +97,7 @@ public interface LoanProductConstants { String recalculationCompoundingFrequencyNthDayParamName = "recalculationCompoundingFrequencyNthDayType"; String recalculationCompoundingFrequencyOnDayParamName = "recalculationCompoundingFrequencyOnDayType"; String isCompoundingToBePostedAsTransactionParamName = "isCompoundingToBePostedAsTransaction"; + String disallowInterestCalculationOnPastDueParamName = "disallowInterestCalculationOnPastDue"; // Guarantee related String holdGuaranteeFundsParamName = "holdGuaranteeFunds"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index e83786baaf1..3dc2f6e2609 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -198,6 +198,8 @@ private PostLoanProductsRequest() {} public Boolean isCompoundingToBePostedAsTransaction; @Schema(example = "false") public Boolean allowCompoundingOnEod; + @Schema(example = "false") + public Boolean disallowInterestCalculationOnPastDue; // Accounting @Schema(example = "3") @@ -518,6 +520,8 @@ private GetLoanProductsPreClosureInterestCalculationStrategy() {} public GetLoanProductsPreClosureInterestCalculationStrategy preClosureInterestCalculationStrategy; @Schema(example = "true") public Boolean isArrearsBasedOnOriginalSchedule; + @Schema(example = "false") + public Boolean disallowInterestCalculationOnPastDue; @Schema(example = "true") public Boolean isCompoundingToBePostedAsTransaction; @Schema(example = "true") diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index dd1273338c5..54798266d81 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -1166,7 +1166,7 @@ calendarData, getRecalculationRestFrequencyType(), getRecalculationRestFrequency getInterestRecalculationRestOnDayType(), compoundingCalendarData, getRecalculationCompoundingFrequencyType(), getRecalculationCompoundingFrequencyInterval(), getInterestRecalculationCompoundingNthDayType(), getInterestRecalculationCompoundingWeekDayType(), getInterestRecalculationCompoundingOnDayType(), - isCompoundingToBePostedAsTransaction(), allowCompoundingOnEod()); + isCompoundingToBePostedAsTransaction(), allowCompoundingOnEod(), disallowInterestCalculationOnPastDue()); } private EnumOptionData getRescheduleStrategyType() { @@ -1263,6 +1263,11 @@ public Boolean allowCompoundingOnEod() { 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; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductInterestRecalculationData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductInterestRecalculationData.java index 8c6fe41100d..9820a631613 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductInterestRecalculationData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductInterestRecalculationData.java @@ -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 LoanProductInterestRecalculationData(final Long id, final Long productId, 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 LoanProductInterestRecalculationData(final Long id, final Long productId, this.preClosureInterestCalculationStrategy = preCloseInterestCalculationStrategy; this.isCompoundingToBePostedAsTransaction = isCompoundingToBePostedAsTransaction; this.allowCompoundingOnEod = allowCompoundingOnEod; + this.disallowInterestCalculationOnPastDue = disallowInterestCalculationOnPastDue; } public static LoanProductInterestRecalculationData sensibleDefaultsForNewLoanProductCreation() { @@ -101,12 +104,15 @@ public static LoanProductInterestRecalculationData sensibleDefaultsForNewLoanPro 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 boolean isIsArrearsBasedOnOriginalSchedule() { public boolean isIsCompoundingToBePostedAsTransaction() { return isCompoundingToBePostedAsTransaction; } + + public Boolean disallowInterestCalculationOnPastDue() { + return disallowInterestCalculationOnPastDue; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductInterestRecalculationDetails.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductInterestRecalculationDetails.java index b46c1bae5be..ecca9c1ddc5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductInterestRecalculationDetails.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductInterestRecalculationDetails.java @@ -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 static LoanProductInterestRecalculationDetails createFrom(final JsonComma 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 @@ private LoanProductInterestRecalculationDetails(final Integer interestRecalculat 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 @@ private LoanProductInterestRecalculationDetails(final Integer interestRecalculat this.preClosureInterestCalculationStrategy = preCloseInterestCalculationStrategy; this.isCompoundingToBePostedAsTransaction = isCompoundingToBePostedAsTransaction; this.allowCompoundingOnEod = allowCompoundingOnEod; + this.disallowInterestCalculationOnPastDue = disallowInterestCalculationOnPastDue; } public void updateProduct(final LoanProduct loanProduct) { @@ -406,6 +414,13 @@ public void update(final JsonCommand command, final Map actualCh 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 Boolean getIsCompoundingToBePostedAsTransaction() { public Boolean allowCompoundingOnEod() { return this.allowCompoundingOnEod; } + + public Boolean disallowInterestCalculationOnPastDue() { + return disallowInterestCalculationOnPastDue; + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index dc35c20c327..8c180124cf0 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -982,6 +982,10 @@ private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransa 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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 652fe67f03a..3f4d9aa0bcb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -694,6 +694,7 @@ public String loanSchema() { + " 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 LoanAccountData mapRow(final ResultSet rs, @SuppressWarnings("unused") fi 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"); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index e0d45318ed1..9447ace8bf2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -173,7 +173,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, @@ -1057,6 +1058,12 @@ private void validateInterestRecalculationParams(final JsonElement element, fina 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( diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index b63935129a2..6068c7ecf62 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -258,7 +258,9 @@ public String loanProductSchema() { + "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, " @@ -475,12 +477,14 @@ public LoanProductData mapRow(@NotNull final ResultSet rs, @SuppressWarnings("un 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"); diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 340df505e94..2a674e645b5 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -173,4 +173,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0155_add_disallow_interest_calc_on_past_due_field.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0155_add_disallow_interest_calc_on_past_due_field.xml new file mode 100644 index 00000000000..c078794d221 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0155_add_disallow_interest_calc_on_past_due_field.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java index 63395b1b927..c87f5bac649 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRecalculationCOBTest.java @@ -69,7 +69,7 @@ public static void setup() { 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 void verifyLoanInstallmentRecalculatedIfThereIsOverdueInstallmentOnCumula payoffOnDateAndVerifyStatus("1 May 2023", loanIdRef.get()); } + @Test + public void verifyEarlyLateRepaymentOnProgressiveLoanThePastDueHasNoEffect() { + AtomicReference 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); + }); + } + }