This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 7df7be2b16dc3ec4b80dd252d901fc7b49a23052 Author: adam.magyari <[email protected]> AuthorDate: Thu Apr 10 09:04:43 2025 +0200 FINERACT-2232: Progressive loan income capitalization config --- .../LoanCapitalizedIncomeCalculationType.java | 48 +++++++ .../domain/LoanCapitalizedIncomeStrategy.java | 47 +++++++ .../loanschedule/domain/LoanApplicationTerms.java | 47 ++++++- .../loanproduct/LoanProductConstants.java | 5 + .../api/LoanProductsApiResourceSwagger.java | 34 +++++ .../loanproduct/data/LoanProductData.java | 48 ++++++- .../portfolio/loanproduct/domain/LoanProduct.java | 9 +- .../domain/LoanProductRelatedDetail.java | 27 +++- .../loanaccount/api/LoansApiResource.java | 6 +- .../loanaccount/api/LoansApiResourceSwagger.java | 12 ++ .../loanaccount/data/LoanAccountData.java | 21 ++- .../service/LoanScheduleAssembler.java | 5 +- .../loanaccount/service/LoanProductAssembler.java | 11 +- .../LoanProductRelatedDetailUpdateUtil.java | 24 ++++ .../service/LoanReadPlatformServiceImpl.java | 13 +- .../loanproduct/api/LoanProductsApiResource.java | 13 +- .../serialization/LoanProductDataValidator.java | 45 ++++++- .../LoanProductReadPlatformServiceImpl.java | 16 ++- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../tenant/parts/0171_loan_capitalized_income.xml | 49 +++++++ .../domain/DefaultScheduledDateGeneratorTest.java | 5 +- .../fineract/integrationtests/LoanProductTest.java | 147 +++++++++++++++++++++ 22 files changed, 601 insertions(+), 32 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeCalculationType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeCalculationType.java new file mode 100644 index 0000000000..5916b0074e --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeCalculationType.java @@ -0,0 +1,48 @@ +/** + * 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.domain; + +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; + +@Getter +@RequiredArgsConstructor +public enum LoanCapitalizedIncomeCalculationType { + + FLAT("loanCapitalizedIncomeCalculationType.flat", "Flat"); + + private final String code; + private final String humanReadableName; + + public static List<StringEnumOptionData> getValuesAsStringEnumOptionDataList() { + return Arrays.stream(values()).map(v -> new StringEnumOptionData(v.name(), v.getCode(), v.getHumanReadableName())).toList(); + } + + public StringEnumOptionData getValueAsStringEnumOptionData() { + return new StringEnumOptionData(name(), getCode(), getHumanReadableName()); + } + + public static StringEnumOptionData getStringEnumOptionData(String name) { + return name == null || name.trim().isEmpty() ? null + : LoanCapitalizedIncomeCalculationType.valueOf(name).getValueAsStringEnumOptionData(); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeStrategy.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeStrategy.java new file mode 100644 index 0000000000..b0d489ce6b --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCapitalizedIncomeStrategy.java @@ -0,0 +1,47 @@ +/** + * 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.domain; + +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; + +@Getter +@RequiredArgsConstructor +public enum LoanCapitalizedIncomeStrategy { + + EQUAL_AMORTIZATION("capitalizedIncome.strategy.equalAmortization", "Equal amortization"); + + private final String code; + private final String humanReadableName; + + public static List<StringEnumOptionData> getValuesAsStringEnumOptionDataList() { + return Arrays.stream(values()).map(v -> new StringEnumOptionData(v.name(), v.getCode(), v.getHumanReadableName())).toList(); + } + + public StringEnumOptionData getValueAsStringEnumOptionData() { + return new StringEnumOptionData(name(), getCode(), getHumanReadableName()); + } + + public static StringEnumOptionData getStringEnumOptionData(String name) { + return name == null || name.trim().isEmpty() ? null : LoanCapitalizedIncomeStrategy.valueOf(name).getValueAsStringEnumOptionData(); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index 0dcc262483..f517a1b240 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -46,6 +46,8 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanproduct.data.LoanProductRelatedDetailMinimumData; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; @@ -232,6 +234,9 @@ public final class LoanApplicationTerms { private LoanChargeOffBehaviour chargeOffBehaviour; private boolean interestRecognitionOnDisbursementDate; private DaysInYearCustomStrategyType daysInYearCustomStrategy; + private boolean enableIncomeCapitalization; + private LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType; + private LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy; private LoanApplicationTerms(Builder builder) { this.currency = builder.currency; @@ -268,6 +273,9 @@ public final class LoanApplicationTerms { } else { this.downPaymentAmount = Money.zero(getCurrency(), builder.mc); } + this.enableIncomeCapitalization = builder.enableIncomeCapitalization; + this.capitalizedIncomeCalculationType = builder.capitalizedIncomeCalculationType; + this.capitalizedIncomeStrategy = builder.capitalizedIncomeStrategy; } public static class Builder { @@ -297,6 +305,9 @@ public final class LoanApplicationTerms { private MathContext mc; private Boolean interestRecognitionOnDisbursementDate; private DaysInYearCustomStrategyType daysInYearCustomStrategy; + private boolean enableIncomeCapitalization; + private LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType; + private LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy; public Builder currency(CurrencyData currency) { this.currency = currency; @@ -413,6 +424,21 @@ public final class LoanApplicationTerms { return this; } + public Builder enableIncomeCapitalization(boolean value) { + this.enableIncomeCapitalization = value; + return this; + } + + public Builder capitalizedIncomeCalculationType(LoanCapitalizedIncomeCalculationType value) { + this.capitalizedIncomeCalculationType = value; + return this; + } + + public Builder capitalizedIncomeStrategy(LoanCapitalizedIncomeStrategy value) { + this.capitalizedIncomeStrategy = value; + return this; + } + public LoanApplicationTerms build() { return new LoanApplicationTerms(this); } @@ -480,7 +506,9 @@ public final class LoanApplicationTerms { final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, final List<LoanSupportedInterestRefundTypes> supportedInterestRefundTypes, final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate, - final DaysInYearCustomStrategyType daysInYearCustomStrategy) { + final DaysInYearCustomStrategyType daysInYearCustomStrategy, final boolean enableIncomeCapitalization, + final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType, + final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; @@ -500,7 +528,8 @@ public final class LoanApplicationTerms { isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate, daysInYearCustomStrategy); + interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization, + capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } @@ -574,7 +603,9 @@ public final class LoanApplicationTerms { disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, fixedLength, loanProductRelatedDetail.isEnableAccrualActivityPosting(), loanProductRelatedDetail.getSupportedInterestRefundTypes(), loanProductRelatedDetail.getChargeOffBehaviour(), - loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate(), loanProductRelatedDetail.getDaysInYearCustomStrategy()); + loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate(), loanProductRelatedDetail.getDaysInYearCustomStrategy(), + loanProductRelatedDetail.isEnableIncomeCapitalization(), loanProductRelatedDetail.getCapitalizedIncomeCalculationType(), + loanProductRelatedDetail.getCapitalizedIncomeStrategy()); } private LoanApplicationTerms(final CurrencyData currency, final Integer loanTermFrequency, @@ -605,7 +636,9 @@ public final class LoanApplicationTerms { final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, boolean enableAccrualActivityPosting, final List<LoanSupportedInterestRefundTypes> supportedInterestRefundTypes, final LoanChargeOffBehaviour chargeOffBehaviour, - final boolean interestRecognitionOnDisbursementDate, final DaysInYearCustomStrategyType daysInYearCustomStrategy) { + final boolean interestRecognitionOnDisbursementDate, final DaysInYearCustomStrategyType daysInYearCustomStrategy, + final boolean enableIncomeCapitalization, final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType, + final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -707,6 +740,9 @@ public final class LoanApplicationTerms { this.chargeOffBehaviour = chargeOffBehaviour; this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate; this.daysInYearCustomStrategy = daysInYearCustomStrategy; + this.enableIncomeCapitalization = enableIncomeCapitalization; + this.capitalizedIncomeCalculationType = capitalizedIncomeCalculationType; + this.capitalizedIncomeStrategy = capitalizedIncomeStrategy; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -1568,7 +1604,8 @@ public final class LoanApplicationTerms { this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled, this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled, this.loanScheduleType, this.loanScheduleProcessingType, this.fixedLength, this.enableAccrualActivityPosting, this.supportedInterestRefundTypes, - this.chargeOffBehaviour, this.interestRecognitionOnDisbursementDate, this.daysInYearCustomStrategy); + this.chargeOffBehaviour, this.interestRecognitionOnDisbursementDate, this.daysInYearCustomStrategy, + this.enableIncomeCapitalization, this.capitalizedIncomeCalculationType, this.capitalizedIncomeStrategy); } public LoanProductMinimumRepaymentScheduleRelatedDetail toLoanProductRelatedDetailMinimumData() { 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 7ee57241e7..1e803ab0af 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 @@ -171,4 +171,9 @@ public interface LoanProductConstants { String CHARGE_OFF_BEHAVIOUR = "chargeOffBehaviour"; String INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE = "interestRecognitionOnDisbursementDate"; + + // Capitalized income + String ENABLE_INCOME_CAPITALIZATION_PARAM_NAME = "enableIncomeCapitalization"; + String CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME = "capitalizedIncomeCalculationType"; + String CAPITALIZED_INCOME_STRATEGY_PARAM_NAME = "capitalizedIncomeStrategy"; } 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 8e01a143cc..caf9183f79 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 @@ -195,6 +195,12 @@ public final class LoanProductsApiResourceSwagger { public Boolean enableAccrualActivityPosting; @Schema(example = "false") public Boolean interestRecognitionOnDisbursementDate; + @Schema(example = "false") + public Boolean enableIncomeCapitalization; + @Schema(example = "FLAT", allowableValues = "FLAT") + public String capitalizedIncomeCalculationType; + @Schema(example = "EQUAL_AMORTIZATION", allowableValues = "EQUAL_AMORTIZATION") + public String capitalizedIncomeStrategy; // Interest Recalculation @Schema(example = "false") @@ -651,6 +657,12 @@ public final class LoanProductsApiResourceSwagger { public StringEnumOptionData chargeOffBehaviour; @Schema(example = "false") public Boolean interestRecognitionOnDisbursementDate; + @Schema(example = "false") + public Boolean enableIncomeCapitalization; + @Schema(example = "FLAT") + public StringEnumOptionData capitalizedIncomeCalculationType; + @Schema(example = "EQUAL_AMORTIZATION") + public StringEnumOptionData capitalizedIncomeStrategy; } @Schema(description = "GetLoanProductsTemplateResponse") @@ -1097,6 +1109,14 @@ public final class LoanProductsApiResourceSwagger { public List<GetLoanProductsChargeOffReasonOptions> chargeOffReasonOptions; public StringEnumOptionData chargeOffBehaviour; public List<StringEnumOptionData> chargeOffBehaviourOptions; + @Schema(example = "false") + public Boolean enableIncomeCapitalization; + @Schema(example = "FLAT") + public StringEnumOptionData capitalizedIncomeCalculationType; + @Schema(example = "EQUAL_AMORTIZATION") + public StringEnumOptionData capitalizedIncomeStrategy; + public List<StringEnumOptionData> capitalizedIncomeCalculationTypeOptions; + public List<StringEnumOptionData> capitalizedIncomeStrategyOptions; } @Schema(description = "GetLoanProductsProductIdResponse") @@ -1416,6 +1436,14 @@ public final class LoanProductsApiResourceSwagger { public StringEnumOptionData chargeOffBehaviour; @Schema(example = "false") public Boolean interestRecognitionOnDisbursementDate; + @Schema(example = "false") + public Boolean enableIncomeCapitalization; + @Schema(example = "FLAT") + public StringEnumOptionData capitalizedIncomeCalculationType; + @Schema(example = "EQUAL_AMORTIZATION") + public StringEnumOptionData capitalizedIncomeStrategy; + public List<StringEnumOptionData> capitalizedIncomeCalculationTypeOptions; + public List<StringEnumOptionData> capitalizedIncomeStrategyOptions; } @Schema(description = "PutLoanProductsProductIdRequest") @@ -1669,6 +1697,12 @@ public final class LoanProductsApiResourceSwagger { public List<String> supportedInterestRefundTypes; @Schema(example = "REGULAR") public String chargeOffBehaviour; + @Schema(example = "false") + public Boolean enableIncomeCapitalization; + @Schema(example = "FLAT", allowableValues = "FLAT") + public String capitalizedIncomeCalculationType; + @Schema(example = "EQUAL_AMORTIZATION", allowableValues = "EQUAL_AMORTIZATION") + public String capitalizedIncomeStrategy; } public static final class AdvancedPaymentData { 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 bd579d9840..4ed7c23820 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 @@ -48,6 +48,8 @@ import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.floatingrates.data.FloatingRateData; import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -238,6 +240,11 @@ public class LoanProductData implements Serializable { private final boolean interestRecognitionOnDisbursementDate; private final List<StringEnumOptionData> daysInYearCustomStrategyOptions; private final StringEnumOptionData daysInYearCustomStrategy; + private Boolean enableIncomeCapitalization; + private StringEnumOptionData capitalizedIncomeCalculationType; + private StringEnumOptionData capitalizedIncomeStrategy; + private List<StringEnumOptionData> capitalizedIncomeCalculationTypeOptions; + private List<StringEnumOptionData> capitalizedIncomeStrategyOptions; /** * Used when returning lookup information about loan product for dropdowns. @@ -342,6 +349,9 @@ public class LoanProductData implements Serializable { final StringEnumOptionData chargeOffBehaviour = null; final boolean interestRecognitionOnDisbursementDate = false; final StringEnumOptionData daysInYearTypeCustomStrategy = null; + final boolean enableIncomeCapitalization = false; + final StringEnumOptionData capitalizedIncomeCalculationType = null; + final StringEnumOptionData capitalizedIncomeStrategy = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -363,7 +373,8 @@ public class LoanProductData implements Serializable { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy); + interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization, + capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } @@ -468,6 +479,9 @@ public class LoanProductData implements Serializable { final StringEnumOptionData chargeOffBehaviour = null; final boolean interestRecognitionOnDisbursementDate = false; final StringEnumOptionData daysInYearTypeCustomStrategy = null; + final boolean enableIncomeCapitalization = false; + final StringEnumOptionData capitalizedIncomeCalculationType = null; + final StringEnumOptionData capitalizedIncomeStrategy = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -489,7 +503,8 @@ public class LoanProductData implements Serializable { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy); + interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization, + capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } @@ -601,6 +616,9 @@ public class LoanProductData implements Serializable { final StringEnumOptionData chargeOffBehaviour = LoanChargeOffBehaviour.REGULAR.getValueAsStringEnumOptionData(); final boolean interestRecognitionOnDisbursementDate = false; final StringEnumOptionData daysInYearTypeCustomStrategy = null; + final boolean enableIncomeCapitalization = false; + final StringEnumOptionData capitalizedIncomeCalculationType = null; + final StringEnumOptionData capitalizedIncomeStrategy = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -622,7 +640,8 @@ public class LoanProductData implements Serializable { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy); + interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization, + capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } @@ -728,6 +747,9 @@ public class LoanProductData implements Serializable { final StringEnumOptionData chargeOffBehaviour = null; final boolean interestRecognitionOnDisbursementDate = false; final StringEnumOptionData daysInYearTypeCustomStrategy = null; + final boolean enableIncomeCapitalization = false; + final StringEnumOptionData capitalizedIncomeCalculationType = null; + final StringEnumOptionData capitalizedIncomeStrategy = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -749,7 +771,8 @@ public class LoanProductData implements Serializable { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy); + interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy, enableIncomeCapitalization, + capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map<String, Object> accountingMappings, @@ -804,7 +827,8 @@ public class LoanProductData implements Serializable { final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, final List<StringEnumOptionData> supportedInterestRefundTypes, StringEnumOptionData chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate, - final StringEnumOptionData daysInYearCustomStrategy) { + final StringEnumOptionData daysInYearCustomStrategy, final boolean enableIncomeCapitalization, + final StringEnumOptionData capitalizedIncomeCalculationType, final StringEnumOptionData capitalizedIncomeStrategy) { this.id = id; this.name = name; this.shortName = shortName; @@ -861,6 +885,9 @@ public class LoanProductData implements Serializable { this.rates = rates; this.isRatesEnabled = isRatesEnabled; this.daysInYearCustomStrategy = daysInYearCustomStrategy; + this.enableIncomeCapitalization = enableIncomeCapitalization; + this.capitalizedIncomeCalculationType = capitalizedIncomeCalculationType; + this.capitalizedIncomeStrategy = capitalizedIncomeStrategy; this.chargeOptions = null; this.penaltyOptions = null; @@ -950,6 +977,8 @@ public class LoanProductData implements Serializable { this.chargeOffReasonOptions = null; this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate; this.daysInYearCustomStrategyOptions = DaysInYearCustomStrategyType.getValuesAsStringEnumOptionDataList(); + this.capitalizedIncomeCalculationTypeOptions = LoanCapitalizedIncomeCalculationType.getValuesAsStringEnumOptionDataList(); + this.capitalizedIncomeStrategyOptions = LoanCapitalizedIncomeStrategy.getValuesAsStringEnumOptionDataList(); } public LoanProductData(final LoanProductData productData, final Collection<ChargeData> chargeOptions, @@ -974,7 +1003,9 @@ public class LoanProductData implements Serializable { final List<EnumOptionData> creditAllocationAllocationTypes, final List<StringEnumOptionData> supportedInterestRefundTypesOptions, final List<StringEnumOptionData> chargeOffBehaviourOptions, final List<CodeValueData> chargeOffReasonOptions, - final List<StringEnumOptionData> daysInYearCustomStrategyOptions) { + final List<StringEnumOptionData> daysInYearCustomStrategyOptions, + final List<StringEnumOptionData> capitalizedIncomeCalculationTypeOptions, + final List<StringEnumOptionData> capitalizedIncomeStrategyOptions) { this.id = productData.id; this.name = productData.name; @@ -1138,6 +1169,11 @@ public class LoanProductData implements Serializable { this.interestRecognitionOnDisbursementDate = productData.interestRecognitionOnDisbursementDate; this.daysInYearCustomStrategyOptions = daysInYearCustomStrategyOptions; this.daysInYearCustomStrategy = productData.daysInYearCustomStrategy; + this.enableIncomeCapitalization = productData.enableIncomeCapitalization; + this.capitalizedIncomeCalculationType = productData.capitalizedIncomeCalculationType; + this.capitalizedIncomeStrategy = productData.capitalizedIncomeStrategy; + this.capitalizedIncomeCalculationTypeOptions = capitalizedIncomeCalculationTypeOptions; + this.capitalizedIncomeStrategyOptions = capitalizedIncomeStrategyOptions; } private Collection<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index c9b85ac25f..31e4ab8a1d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -59,6 +59,8 @@ import org.apache.fineract.portfolio.floatingrates.data.FloatingRateDTO; import org.apache.fineract.portfolio.floatingrates.data.FloatingRatePeriodData; import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate; import org.apache.fineract.portfolio.fund.domain.Fund; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -280,7 +282,9 @@ public class LoanProduct extends AbstractPersistableCustom<Long> { final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, final List<LoanSupportedInterestRefundTypes> supportedInterestRefundTypes, final LoanChargeOffBehaviour chargeOffBehaviour, final boolean isInterestRecognitionOnDisbursementDate, - final DaysInYearCustomStrategyType daysInYearCustomStrategy) { + final DaysInYearCustomStrategyType daysInYearCustomStrategy, final boolean enableIncomeCapitalization, + final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType, + final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy) { this.fund = fund; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; @@ -330,7 +334,8 @@ public class LoanProduct extends AbstractPersistableCustom<Long> { inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, - supportedInterestRefundTypes, chargeOffBehaviour, isInterestRecognitionOnDisbursementDate, daysInYearCustomStrategy); + supportedInterestRefundTypes, chargeOffBehaviour, isInterestRecognitionOnDisbursementDate, daysInYearCustomStrategy, + enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy); this.loanProductMinMaxConstraints = new LoanProductMinMaxConstraints(defaultMinPrincipal, defaultMaxPrincipal, defaultMinNominalInterestRatePerPeriod, defaultMaxNominalInterestRatePerPeriod, defaultMinNumberOfInstallments, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index a1a68ae214..29b18e6cea 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -36,6 +36,8 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -168,6 +170,17 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "days_in_year_custom_strategy") private DaysInYearCustomStrategyType daysInYearCustomStrategy; + @Column(name = "enable_income_capitalization") + private boolean enableIncomeCapitalization = false; + + @Enumerated(EnumType.STRING) + @Column(name = "capitalized_income_calculation_type") + private LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType; + + @Enumerated(EnumType.STRING) + @Column(name = "capitalized_income_strategy") + private LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy; + public static LoanProductRelatedDetail createFrom(final CurrencyData currencyData, final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, @@ -182,7 +195,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, final List<LoanSupportedInterestRefundTypes> supportedInterestRefundTypes, final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate, - final DaysInYearCustomStrategyType daysInYearCustomStrategy) { + final DaysInYearCustomStrategyType daysInYearCustomStrategy, final boolean enableIncomeCapitalization, + final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType, + final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy) { final MonetaryCurrency currency = MonetaryCurrency.fromCurrencyData(currencyData); return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, @@ -192,7 +207,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, - chargeOffBehaviour, interestRecognitionOnDisbursementDate, daysInYearCustomStrategy); + chargeOffBehaviour, interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization, + capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } protected LoanProductRelatedDetail() { @@ -213,7 +229,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, List<LoanSupportedInterestRefundTypes> supportedInterestRefundTypes, final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate, - final DaysInYearCustomStrategyType daysInYearCustomStrategy) { + final DaysInYearCustomStrategyType daysInYearCustomStrategy, final boolean enableIncomeCapitalization, + final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType, + final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy) { this.currency = currency; this.principal = defaultPrincipal; this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; @@ -251,6 +269,9 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.chargeOffBehaviour = chargeOffBehaviour; this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate; this.daysInYearCustomStrategy = daysInYearCustomStrategy; + this.enableIncomeCapitalization = enableIncomeCapitalization; + this.capitalizedIncomeCalculationType = capitalizedIncomeCalculationType; + this.capitalizedIncomeStrategy = capitalizedIncomeStrategy; } private Integer defaultToNullIfZero(final Integer value) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index ef4a8c075f..b57888bc03 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -132,6 +132,8 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; @@ -1135,7 +1137,9 @@ public class LoansApiResource { loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations, overdueCharges, paidInAdvanceTemplate, interestRatesPeriods, clientActiveLoanOptions, rates, isRatesEnabled, collectionData, LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), - loanTermVariations, DaysInYearCustomStrategyType.getValuesAsStringEnumOptionDataList()); + loanTermVariations, DaysInYearCustomStrategyType.getValuesAsStringEnumOptionDataList(), + LoanCapitalizedIncomeCalculationType.getValuesAsStringEnumOptionDataList(), + LoanCapitalizedIncomeStrategy.getValuesAsStringEnumOptionDataList()); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), mandatoryResponseParameters); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index d549371e4f..17787426d8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -1206,6 +1206,12 @@ final class LoansApiResourceSwagger { public StringEnumOptionData chargeOffBehaviour; @Schema(example = "false") public Boolean interestRecognitionOnDisbursementDate; + @Schema(example = "false") + public Boolean enableIncomeCapitalization; + @Schema(example = "FLAT") + public StringEnumOptionData capitalizedIncomeCalculationType; + @Schema(example = "EQUAL_AMORTIZATION") + public StringEnumOptionData capitalizedIncomeStrategy; } @Schema(description = "GetLoansResponse") @@ -1309,6 +1315,12 @@ final class LoansApiResourceSwagger { public BigDecimal fixedEmiAmount; @Schema(example = "false") public Boolean interestRecognitionOnDisbursementDate; + @Schema(example = "false") + public Boolean enableIncomeCapitalization; + @Schema(example = "FLAT", allowableValues = "FLAT") + public String capitalizedIncomeCalculationType; + @Schema(example = "EQUAL_AMORTIZATION", allowableValues = "EQUAL_AMORTIZATION") + public String capitalizedIncomeStrategy; public List<PostLoansRequestChargeData> charges; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 1d7a6f21c0..825293fdb6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -172,6 +172,8 @@ public class LoanAccountData { private List<EnumOptionData> loanScheduleTypeOptions; private List<EnumOptionData> loanScheduleProcessingTypeOptions; private List<StringEnumOptionData> daysInYearCustomStrategyOptions; + private List<StringEnumOptionData> capitalizedIncomeCalculationTypeOptions; + private List<StringEnumOptionData> capitalizedIncomeStrategyOptions; @Transient private BigDecimal feeChargesAtDisbursementCharged; @@ -274,6 +276,9 @@ public class LoanAccountData { private EnumOptionData loanScheduleProcessingType; private StringEnumOptionData chargeOffBehaviour; + private Boolean enableIncomeCapitalization; + private StringEnumOptionData capitalizedIncomeCalculationType; + private StringEnumOptionData capitalizedIncomeStrategy; public static LoanAccountData importInstanceIndividual(EnumOptionData loanTypeEnumOption, Long clientId, Long productId, Long loanOfficerId, LocalDate submittedOnDate, Long fundId, BigDecimal principal, Integer numberOfRepayments, @@ -464,7 +469,9 @@ public class LoanAccountData { final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final boolean enableInstallmentLevelDelinquency, final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType, final Integer fixedLength, final StringEnumOptionData chargeOffBehaviour, - final boolean isInterestRecognitionOnDisbursementDate, final StringEnumOptionData daysInYearCustomStrategy) { + final boolean isInterestRecognitionOnDisbursementDate, final StringEnumOptionData daysInYearCustomStrategy, + final boolean enableIncomeCapitalization, final StringEnumOptionData capitalizedIncomeCalculationType, + final StringEnumOptionData capitalizedIncomeStrategy) { final CollectionData delinquent = CollectionData.template(); @@ -510,7 +517,9 @@ public class LoanAccountData { .setEnableInstallmentLevelDelinquency(enableInstallmentLevelDelinquency).setLoanScheduleType(loanScheduleType) .setLoanScheduleProcessingType(loanScheduleProcessingType).setFixedLength(fixedLength) .setChargeOffBehaviour(chargeOffBehaviour).setInterestRecognitionOnDisbursementDate(isInterestRecognitionOnDisbursementDate) - .setDaysInYearCustomStrategy(daysInYearCustomStrategy); + .setDaysInYearCustomStrategy(daysInYearCustomStrategy).setEnableIncomeCapitalization(enableIncomeCapitalization) + .setCapitalizedIncomeCalculationType(capitalizedIncomeCalculationType) + .setCapitalizedIncomeStrategy(capitalizedIncomeStrategy); } /* @@ -536,7 +545,9 @@ public class LoanAccountData { final Collection<LoanAccountSummaryData> clientActiveLoanOptions, final List<RateData> rates, final Boolean isRatesEnabled, final CollectionData delinquent, final List<EnumOptionData> loanScheduleTypeOptions, final List<EnumOptionData> loanScheduleProcessingTypeOptions, final List<LoanTermVariationsData> loanTermVariations, - final List<StringEnumOptionData> daysInYearCustomStrategyOptions) { + final List<StringEnumOptionData> daysInYearCustomStrategyOptions, + final List<StringEnumOptionData> capitalizedIncomeCalculationTypeOptions, + final List<StringEnumOptionData> capitalizedIncomeStrategyOptions) { // TODO: why are these variables 'calendarData', 'chargeTemplate' never used (see original private constructor) @@ -557,7 +568,9 @@ public class LoanAccountData { .setClientActiveLoanOptions(clientActiveLoanOptions).setRates(rates).setIsRatesEnabled(isRatesEnabled) .setDelinquent(delinquent).setLoanScheduleTypeOptions(loanScheduleTypeOptions) .setLoanScheduleProcessingTypeOptions(loanScheduleProcessingTypeOptions).setLoanTermVariations(loanTermVariations) - .setDaysInYearCustomStrategyOptions(daysInYearCustomStrategyOptions); + .setDaysInYearCustomStrategyOptions(daysInYearCustomStrategyOptions) + .setCapitalizedIncomeCalculationTypeOptions(capitalizedIncomeCalculationTypeOptions) + .setCapitalizedIncomeStrategyOptions(capitalizedIncomeStrategyOptions); } public LoanAccountData associationsAndTemplate(final Collection<LoanProductData> productOptions, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index f6fb5578f3..b4ac0b05c5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -544,7 +544,10 @@ public class LoanScheduleAssembler { loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting(), loanProduct.getLoanProductRelatedDetail().getSupportedInterestRefundTypes(), loanProduct.getLoanProductRelatedDetail().getChargeOffBehaviour(), interestRecognitionOnDisbursementDate, - loanProduct.getLoanProductRelatedDetail().getDaysInYearCustomStrategy()); + loanProduct.getLoanProductRelatedDetail().getDaysInYearCustomStrategy(), + loanProduct.getLoanProductRelatedDetail().isEnableIncomeCapitalization(), + loanProduct.getLoanProductRelatedDetail().getCapitalizedIncomeCalculationType(), + loanProduct.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy()); } private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductAssembler.java index 67b748065d..85522bed1a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductAssembler.java @@ -40,6 +40,8 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate; import org.apache.fineract.portfolio.fund.domain.Fund; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; @@ -305,6 +307,13 @@ public class LoanProductAssembler { chargeOffBehaviour = LoanChargeOffBehaviour.REGULAR; } + final boolean enableIncomeCapitalization = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME); + final LoanCapitalizedIncomeCalculationType capitalizedIncomeCalculationType = command.enumValueOfParameterNamed( + LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, LoanCapitalizedIncomeCalculationType.class); + final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy = command.enumValueOfParameterNamed( + LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, LoanCapitalizedIncomeStrategy.class); + return new LoanProduct(fund, loanTransactionProcessingStrategy, loanProductPaymentAllocationRules, loanProductCreditAllocationRules, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, interestRatePerPeriod, minInterestRatePerPeriod, maxInterestRatePerPeriod, interestFrequencyType, annualInterestRate, interestMethod, @@ -325,7 +334,7 @@ public class LoanProductAssembler { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate, - daysInYearCustomStrategy); + daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java index 9e286fe24d..da78ccf984 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanProductRelatedDetailUpdateUtil.java @@ -26,6 +26,8 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -296,6 +298,28 @@ public class LoanProductRelatedDetailUpdateUtil { loanRepaymentScheduleDetail.updateInterestRecognitionOnDisbursementDate(newValue); } + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME, + loanRepaymentScheduleDetail.isEnableIncomeCapitalization())) { + final boolean newValue = command + .booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME); + actualChanges.put(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME, newValue); + loanRepaymentScheduleDetail.setEnableIncomeCapitalization(newValue); + } + + if (command.parameterExists(LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME)) { + final LoanCapitalizedIncomeCalculationType newValue = command.enumValueOfParameterNamed( + LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, LoanCapitalizedIncomeCalculationType.class); + actualChanges.put(LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, newValue); + loanRepaymentScheduleDetail.setCapitalizedIncomeCalculationType(newValue); + } + + if (command.parameterExists(LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME)) { + final LoanCapitalizedIncomeStrategy newValue = command.enumValueOfParameterNamed( + LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, LoanCapitalizedIncomeStrategy.class); + actualChanges.put(LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, newValue); + loanRepaymentScheduleDetail.setCapitalizedIncomeStrategy(newValue); + } + return actualChanges; } 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 f5f61cdf2b..e2399aeeab 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 @@ -106,6 +106,8 @@ import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; @@ -722,6 +724,9 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa + " l.is_floating_interest_rate as isFloatingInterestRate, " + " l.interest_rate_differential as interestRateDifferential, " + " l.days_in_year_custom_strategy as daysInYearCustomStrategy, " + + " l.enable_income_capitalization as enableIncomeCapitalization, " + + " l.capitalized_income_calculation_type as capitalizedIncomeCalculationType, " + + " l.capitalized_income_strategy as capitalizedIncomeStrategy, " + " l.create_standing_instruction_at_disbursement as createStandingInstructionAtDisbursement, " + " lpvi.minimum_gap as minimuminstallmentgap, lpvi.maximum_gap as maximuminstallmentgap, " + " lp.can_use_for_topup as canUseForTopup, l.is_topup as isTopup, topup.closure_loan_id as closureLoanId, " @@ -1098,6 +1103,11 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final boolean interestRecognitionOnDisbursementDate = rs.getBoolean("interestRecognitionOnDisbursementDate"); final StringEnumOptionData daysInYearCustomStrategy = DaysInYearCustomStrategyType .getStringEnumOptionData(rs.getString("daysInYearCustomStrategy")); + final boolean enableIncomeCapitalization = rs.getBoolean("enableIncomeCapitalization"); + final StringEnumOptionData capitalizedIncomeCalculationType = LoanCapitalizedIncomeCalculationType + .getStringEnumOptionData(rs.getString("capitalizedIncomeCalculationType")); + final StringEnumOptionData capitalizedIncomeStrategy = LoanCapitalizedIncomeStrategy + .getStringEnumOptionData(rs.getString("capitalizedIncomeStrategy")); return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, clientExternalId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, @@ -1117,7 +1127,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa lastClosedBusinessDate, overpaidOnDate, isChargedOff, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData(), fixedLength, chargeOffBehaviour.getValueAsStringEnumOptionData(), - interestRecognitionOnDisbursementDate, daysInYearCustomStrategy); + interestRecognitionOnDisbursementDate, daysInYearCustomStrategy, enableIncomeCapitalization, + capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index f45322979d..271b1f7107 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -81,6 +81,8 @@ import org.apache.fineract.portfolio.floatingrates.service.FloatingRatesReadPlat import org.apache.fineract.portfolio.fund.data.FundData; import org.apache.fineract.portfolio.fund.service.FundReadPlatformService; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -127,7 +129,10 @@ public class LoanProductsApiResource { LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, - LoanProductConstants.REPAYMENT_START_DATE_TYPE, LoanProductConstants.DAYS_IN_YEAR_CUSTOM_STRATEGY_TYPE_PARAMETER_NAME)); + LoanProductConstants.REPAYMENT_START_DATE_TYPE, LoanProductConstants.DAYS_IN_YEAR_CUSTOM_STRATEGY_TYPE_PARAMETER_NAME, + LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME, + LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, + LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME)); private static final Set<String> PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>( Arrays.asList("productId", "productName", "restrictedProducts", "allowedProducts", "productOptions")); @@ -439,6 +444,10 @@ public class LoanProductsApiResource { .retrieveCodeValuesByCode(LoanApiConstants.CHARGE_OFF_REASONS); final List<StringEnumOptionData> daysInYearCustomStrategyOptions = DaysInYearCustomStrategyType .getValuesAsStringEnumOptionDataList(); + final List<StringEnumOptionData> capitalizedIncomeCalculationTypeOptions = LoanCapitalizedIncomeCalculationType + .getValuesAsStringEnumOptionDataList(); + final List<StringEnumOptionData> capitalizedIncomeStrategyOptions = LoanCapitalizedIncomeStrategy + .getValuesAsStringEnumOptionDataList(); return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions, @@ -451,7 +460,7 @@ public class LoanProductsApiResource { advancedPaymentAllocationTypes, LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes, creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions, - daysInYearCustomStrategyOptions); + daysInYearCustomStrategyOptions, capitalizedIncomeCalculationTypeOptions, capitalizedIncomeStrategyOptions); } } 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 d2e29d0add..81c8d5b878 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 @@ -49,6 +49,8 @@ import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; @@ -186,7 +188,10 @@ public final class LoanProductDataValidator { LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH, LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES, LoanProductConstants.CHARGE_OFF_BEHAVIOUR, LoanProductConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE, - LoanProductConstants.DAYS_IN_YEAR_CUSTOM_STRATEGY_TYPE_PARAMETER_NAME)); + LoanProductConstants.DAYS_IN_YEAR_CUSTOM_STRATEGY_TYPE_PARAMETER_NAME, + LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME, + LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, + LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME)); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, @@ -877,6 +882,8 @@ public final class LoanProductDataValidator { "Charge off behaviour is only supported for Progressive loans"); } + validateIncomeCapitalization(transactionProcessingStrategyCode, element, baseDataValidator); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -1922,6 +1929,9 @@ public final class LoanProductDataValidator { validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); + + validateIncomeCapitalization(transactionProcessingStrategyCode, element, baseDataValidator); + throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -2660,6 +2670,39 @@ public final class LoanProductDataValidator { } } + private void validateIncomeCapitalization(String transactionProcessingStrategyCode, JsonElement element, + DataValidatorBuilder baseDataValidator) { + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, element)) { + final String capitalizedIncomeCalculationType = this.fromApiJsonHelper + .extractStringNamed(LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME, element); + baseDataValidator.reset().parameter(LoanProductConstants.CAPITALIZED_INCOME_CALCULATION_TYPE_PARAM_NAME) + .value(capitalizedIncomeCalculationType).isOneOfEnumValues(LoanCapitalizedIncomeCalculationType.class); + } + + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, element)) { + final String capitalizedIncomeStrategy = this.fromApiJsonHelper + .extractStringNamed(LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME, element); + baseDataValidator.reset().parameter(LoanProductConstants.CAPITALIZED_INCOME_STRATEGY_PARAM_NAME) + .value(capitalizedIncomeStrategy).isOneOfEnumValues(LoanCapitalizedIncomeStrategy.class); + } + + if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategyCode) + && this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME, element)) { + Boolean enableIncomeCapitalization = this.fromApiJsonHelper + .extractBooleanNamed(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME, element); + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME) + .value(enableIncomeCapitalization).ignoreIfNull().validateForBooleanValue(); + } else if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME, element)) { + Boolean enableIncomeCapitalization = this.fromApiJsonHelper + .extractBooleanNamed(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME, element); + if (Boolean.TRUE.equals(enableIncomeCapitalization)) { + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INCOME_CAPITALIZATION_PARAM_NAME).failWithCode( + "supported.only.for.progressive.loan.income.capitalization", + "Income capitalization is only supported for Progressive loans"); + } + } + } + private Integer defaultToZeroIfNull(Integer value) { return value != null ? value : 0; } 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 c3fe4c728f..4c436216c1 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 @@ -45,6 +45,8 @@ import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; @@ -288,8 +290,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lp.allow_variabe_installments as isVariableIntallmentsAllowed, " + "lvi.minimum_gap as minimumGap, " + "lvi.maximum_gap as maximumGap, dbuc.id as delinquencyBucketId, dbuc.name as delinquencyBucketName, " + "lp.can_use_for_topup as canUseForTopup, lp.is_equal_amortization as isEqualAmortization, lp.loan_schedule_type as loanScheduleType, lp.loan_schedule_processing_type as loanScheduleProcessingType, lp.supported_interest_refund_types as supportedInterestRefundTypes, " - + "lp.charge_off_behaviour as chargeOffBehaviour" + " from m_product_loan lp " - + " left join m_fund f on f.id = lp.fund_id " + + "lp.charge_off_behaviour as chargeOffBehaviour, " // + + "lp.enable_income_capitalization as enableIncomeCapitalization, " // + + "lp.capitalized_income_calculation_type as capitalizedIncomeCalculationType, " // + + "lp.capitalized_income_strategy as capitalizedIncomeStrategy " // + + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " + " left join m_product_loan_recalculation_details lpr on lpr.product_id=lp.id " + " left join m_product_loan_guarantee_details lpg on lpg.loan_product_id=lp.id " + " left join m_product_loan_configurable_attributes lca on lca.loan_product_id = lp.id " @@ -552,6 +557,11 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final String chargeOffBehaviourStr = rs.getString("chargeOffBehaviour"); final LoanChargeOffBehaviour loanChargeOffBehaviour = LoanChargeOffBehaviour.valueOf(chargeOffBehaviourStr); final boolean interestRecognitionOnDisbursementDate = rs.getBoolean("interestRecognitionOnDisbursementDate"); + final boolean enableIncomeCapitalization = rs.getBoolean("enableIncomeCapitalization"); + final StringEnumOptionData capitalizedIncomeCalculationType = LoanCapitalizedIncomeCalculationType + .getStringEnumOptionData(rs.getString("capitalizedIncomeCalculationType")); + final StringEnumOptionData capitalizedIncomeStrategy = LoanCapitalizedIncomeStrategy + .getStringEnumOptionData(rs.getString("capitalizedIncomeStrategy")); return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -576,7 +586,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData(), fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, loanChargeOffBehaviour.getValueAsStringEnumOptionData(), interestRecognitionOnDisbursementDate, - daysInYearCustomStrategy); + daysInYearCustomStrategy, enableIncomeCapitalization, capitalizedIncomeCalculationType, capitalizedIncomeStrategy); } } 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 2f294c33ce..09a9f0e42e 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 @@ -189,4 +189,5 @@ <include file="parts/0168_transaction_summary_with_asset_owner_report_add_active_intermediate_filtering.xml" relativeToChangelogFile="true" /> <include file="parts/0169_add_missing_permissions.xml" relativeToChangelogFile="true" /> <include file="parts/0170_days_in_year_custom_strategy.xml" relativeToChangelogFile="true"/> + <include file="parts/0171_loan_capitalized_income.xml" relativeToChangelogFile="true"/> </databaseChangeLog> diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0171_loan_capitalized_income.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0171_loan_capitalized_income.xml new file mode 100644 index 0000000000..3ac0cf5a20 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0171_loan_capitalized_income.xml @@ -0,0 +1,49 @@ +<?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 author="fineract" id="1"> + <addColumn tableName="m_loan"> + <column name="enable_income_capitalization" type="boolean" defaultValueBoolean="false"> + <constraints nullable="false"/> + </column> + <column name="capitalized_income_calculation_type" type="VARCHAR(100)"> + <constraints nullable="true"/> + </column> + <column name="capitalized_income_strategy" type="VARCHAR(100)"> + <constraints nullable="true"/> + </column> + </addColumn> + <addColumn tableName="m_product_loan"> + <column name="enable_income_capitalization" type="boolean" defaultValueBoolean="false"> + <constraints nullable="false"/> + </column> + <column name="capitalized_income_calculation_type" type="VARCHAR(100)"> + <constraints nullable="true"/> + </column> + <column name="capitalized_income_strategy" type="VARCHAR(100)"> + <constraints nullable="true"/> + </column> + </addColumn> + </changeSet> +</databaseChangeLog> diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java index a7574fd5f2..d0d83caa6c 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java @@ -98,7 +98,8 @@ public class DefaultScheduledDateGeneratorTest { Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, DISBURSEMENT_DATE, - submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null, false, null); + submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null, false, null, false, null, + null); // when List<? extends LoanScheduleModelPeriod> result = underTest.generateRepaymentPeriods(mathContext, expectedDisbursementDate, @@ -179,7 +180,7 @@ public class DefaultScheduledDateGeneratorTest { EMPTY_LIST, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null, - false, null); + false, null, false, null, null); } private HolidayDetailDTO createHolidayDTO() { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTest.java new file mode 100644 index 0000000000..ea49812563 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductTest.java @@ -0,0 +1,147 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import java.math.BigDecimal; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeCalculationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class LoanProductTest extends BaseLoanIntegrationTest { + + @Test + public void testIncomeCapitalizationEnabled() { + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)); + + final GetLoanProductsProductIdResponse loanProductsProductIdResponse = loanProductHelper + .retrieveLoanProductById(loanProductsResponse.getResourceId()); + Assertions.assertEquals(Boolean.TRUE, loanProductsProductIdResponse.getEnableIncomeCapitalization()); + Assertions.assertNotNull(loanProductsProductIdResponse.getCapitalizedIncomeCalculationType()); + Assertions.assertEquals(LoanCapitalizedIncomeCalculationType.FLAT.getCode(), + loanProductsProductIdResponse.getCapitalizedIncomeCalculationType().getCode()); + Assertions.assertNotNull(loanProductsProductIdResponse.getCapitalizedIncomeStrategy()); + Assertions.assertEquals(LoanCapitalizedIncomeStrategy.EQUAL_AMORTIZATION.getCode(), + loanProductsProductIdResponse.getCapitalizedIncomeStrategy().getCode()); + + runAt("20 December 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024", + 430.0, 7.0, 6, null); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(Boolean.TRUE, loanDetails.getEnableIncomeCapitalization()); + Assertions.assertNotNull(loanDetails.getCapitalizedIncomeCalculationType()); + Assertions.assertEquals(LoanCapitalizedIncomeCalculationType.FLAT.getCode(), + loanDetails.getCapitalizedIncomeCalculationType().getCode()); + Assertions.assertNotNull(loanDetails.getCapitalizedIncomeStrategy()); + Assertions.assertEquals(LoanCapitalizedIncomeStrategy.EQUAL_AMORTIZATION.getCode(), + loanDetails.getCapitalizedIncomeStrategy().getCode()); + + Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, BigDecimal.valueOf(430), "20 December 2024")); + }); + } + + @Test + public void testIncomeCapitalizationDisabled() { + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableIncomeCapitalization(false) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)); + + final GetLoanProductsProductIdResponse loanProductsProductIdResponse = loanProductHelper + .retrieveLoanProductById(loanProductsResponse.getResourceId()); + Assertions.assertEquals(Boolean.FALSE, loanProductsProductIdResponse.getEnableIncomeCapitalization()); + Assertions.assertNotNull(loanProductsProductIdResponse.getCapitalizedIncomeCalculationType()); + Assertions.assertEquals(LoanCapitalizedIncomeCalculationType.FLAT.getCode(), + loanProductsProductIdResponse.getCapitalizedIncomeCalculationType().getCode()); + Assertions.assertNotNull(loanProductsProductIdResponse.getCapitalizedIncomeStrategy()); + Assertions.assertEquals(LoanCapitalizedIncomeStrategy.EQUAL_AMORTIZATION.getCode(), + loanProductsProductIdResponse.getCapitalizedIncomeStrategy().getCode()); + + runAt("20 December 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024", + 430.0, 7.0, 6, null); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(Boolean.FALSE, loanDetails.getEnableIncomeCapitalization()); + Assertions.assertNotNull(loanDetails.getCapitalizedIncomeCalculationType()); + Assertions.assertEquals(LoanCapitalizedIncomeCalculationType.FLAT.getCode(), + loanDetails.getCapitalizedIncomeCalculationType().getCode()); + Assertions.assertNotNull(loanDetails.getCapitalizedIncomeStrategy()); + Assertions.assertEquals(LoanCapitalizedIncomeStrategy.EQUAL_AMORTIZATION.getCode(), + loanDetails.getCapitalizedIncomeStrategy().getCode()); + + Assertions.assertDoesNotThrow(() -> disburseLoan(loanId, BigDecimal.valueOf(430), "20 December 2024")); + }); + } + + @Test + public void testIncomeCapitalizationUpdateProduct() { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)); + + final GetLoanProductsProductIdResponse loanProductsProductIdResponse = loanProductHelper + .retrieveLoanProductById(loanProductsResponse.getResourceId()); + Assertions.assertEquals(Boolean.TRUE, loanProductsProductIdResponse.getEnableIncomeCapitalization()); + Assertions.assertNotNull(loanProductsProductIdResponse.getCapitalizedIncomeCalculationType()); + Assertions.assertEquals(LoanCapitalizedIncomeCalculationType.FLAT.getCode(), + loanProductsProductIdResponse.getCapitalizedIncomeCalculationType().getCode()); + Assertions.assertNotNull(loanProductsProductIdResponse.getCapitalizedIncomeStrategy()); + Assertions.assertEquals(LoanCapitalizedIncomeStrategy.EQUAL_AMORTIZATION.getCode(), + loanProductsProductIdResponse.getCapitalizedIncomeStrategy().getCode()); + + loanProductHelper.updateLoanProductById(loanProductsResponse.getResourceId(), + new PutLoanProductsProductIdRequest().enableIncomeCapitalization(false)); + + final GetLoanProductsProductIdResponse updatedLoanProductsProductIdResponse = loanProductHelper + .retrieveLoanProductById(loanProductsResponse.getResourceId()); + Assertions.assertEquals(Boolean.FALSE, updatedLoanProductsProductIdResponse.getEnableIncomeCapitalization()); + Assertions.assertNotNull(updatedLoanProductsProductIdResponse.getCapitalizedIncomeCalculationType()); + Assertions.assertEquals(LoanCapitalizedIncomeCalculationType.FLAT.getCode(), + updatedLoanProductsProductIdResponse.getCapitalizedIncomeCalculationType().getCode()); + Assertions.assertNotNull(updatedLoanProductsProductIdResponse.getCapitalizedIncomeStrategy()); + Assertions.assertEquals(LoanCapitalizedIncomeStrategy.EQUAL_AMORTIZATION.getCode(), + updatedLoanProductsProductIdResponse.getCapitalizedIncomeStrategy().getCode()); + } + + @Test + public void testIncomeCapitalizationCumulativeNotSupported() { + Assertions.assertThrows(RuntimeException.class, + () -> loanProductHelper.createLoanProduct(createOnePeriod30DaysPeriodicAccrualProduct(7.0).enableIncomeCapitalization(true) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION))); + } + +}
