diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeOffBehaviour.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeOffBehaviour.java new file mode 100644 index 00000000000..dc575491096 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargeOffBehaviour.java @@ -0,0 +1,45 @@ +/** + * 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 LoanChargeOffBehaviour { + + REGULAR("chargeOffBehaviour.regular", "Regular"), // + ZERO_INTEREST("chargeOffBehaviour.zeroInterest", "Zero interest after charge-off"), // + ; + + private final String code; + private final String humanReadableName; + + public static List 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()); + } +} 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 da8560127c4..661076c89a6 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 @@ -47,6 +47,7 @@ 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.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @@ -228,6 +229,7 @@ public final class LoanApplicationTerms { private LoanScheduleProcessingType loanScheduleProcessingType; private boolean enableAccrualActivityPosting; private List supportedInterestRefundTypes; + private LoanChargeOffBehaviour chargeOffBehaviour; private LoanApplicationTerms(Builder builder) { this.currency = builder.currency; @@ -458,7 +460,8 @@ public static LoanApplicationTerms assembleFrom(final ApplicationCurrency curren final Boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes) { + final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, + final LoanChargeOffBehaviour chargeOffBehaviour) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; @@ -477,7 +480,7 @@ public static LoanApplicationTerms assembleFrom(final ApplicationCurrency curren isInterestToBeRecoveredFirstWhenGreaterThanEMI, fixedPrincipalPercentagePerInstallment, isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, - fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); + fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour); } @@ -551,7 +554,7 @@ public static LoanApplicationTerms assembleFrom(final ApplicationCurrency applic isPrincipalCompoundingDisabledForOverdueLoans, isDownPaymentEnabled, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, fixedLength, loanProductRelatedDetail.isEnableAccrualActivityPosting(), - loanProductRelatedDetail.getSupportedInterestRefundTypes()); + loanProductRelatedDetail.getSupportedInterestRefundTypes(), loanProductRelatedDetail.getChargeOffBehaviour()); } private LoanApplicationTerms(final ApplicationCurrency currency, final Integer loanTermFrequency, @@ -581,7 +584,7 @@ private LoanApplicationTerms(final ApplicationCurrency currency, final Integer l final BigDecimal disbursedAmountPercentageForDownPayment, final boolean isAutoRepaymentForDownPaymentEnabled, final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, boolean enableAccrualActivityPosting, - final List supportedInterestRefundTypes) { + final List supportedInterestRefundTypes, final LoanChargeOffBehaviour chargeOffBehaviour) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -680,6 +683,7 @@ private LoanApplicationTerms(final ApplicationCurrency currency, final Integer l this.loanScheduleProcessingType = loanScheduleProcessingType; this.fixedLength = fixedLength; this.supportedInterestRefundTypes = supportedInterestRefundTypes; + this.chargeOffBehaviour = chargeOffBehaviour; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -1540,7 +1544,8 @@ public LoanProductRelatedDetail toLoanProductRelatedDetail() { this.graceOnArrearsAgeing, this.daysInMonthType.getValue(), this.daysInYearType.getValue(), this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled, this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled, this.loanScheduleType, - this.loanScheduleProcessingType, this.fixedLength, this.enableAccrualActivityPosting, this.supportedInterestRefundTypes); + this.loanScheduleProcessingType, this.fixedLength, this.enableAccrualActivityPosting, this.supportedInterestRefundTypes, + this.chargeOffBehaviour); } public Integer getLoanTermFrequency() { 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..55ab6a1ff37 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 @@ -166,5 +166,6 @@ public interface LoanProductConstants { String ENABLE_ACCRUAL_ACTIVITY_POSTING = "enableAccrualActivityPosting"; String SUPPORTED_INTEREST_REFUND_TYPES = "supportedInterestRefundTypes"; + String CHARGE_OFF_BEHAVIOUR = "chargeOffBehaviour"; } 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..a97d1d9c9de 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 @@ -321,6 +321,8 @@ private RateData() {} } public List supportedInterestRefundTypes; + @Schema(example = "REGULAR") + public String chargeOffBehaviour; } @Schema(description = "PostLoanProductsResponse") @@ -604,6 +606,7 @@ private GetLoanProductsAccountingRule() {} public Integer principalThresholdForLastInstalment; public GetLoanProductsResponse.GetLoanProductsRepaymentStartDateType repaymentStartDateType; public List supportedInterestRefundTypes; + public StringEnumOptionData chargeOffBehaviour; } @Schema(description = "GetLoanProductsTemplateResponse") @@ -1088,6 +1091,8 @@ private GetLoanProductsChargeOffReasonOptions() {} public List supportedInterestRefundTypes; public List supportedInterestRefundTypesOptions; public List chargeOffReasonOptions; + public StringEnumOptionData chargeOffBehaviour; + public List chargeOffBehaviourOptions; } @Schema(description = "GetLoanProductsProductIdResponse") @@ -1374,6 +1379,7 @@ private GetLoanCharge() {} public Boolean enableAccrualActivityPosting; public List supportedInterestRefundTypes; public List chargeOffReasonOptions; + public StringEnumOptionData chargeOffBehaviour; } @Schema(description = "PutLoanProductsProductIdRequest") @@ -1667,6 +1673,8 @@ private RateData() {} } public List supportedInterestRefundTypes; + @Schema(example = "REGULAR") + public String chargeOffBehaviour; } 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 dd1273338c5..8ede47ac57e 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 @@ -46,6 +46,7 @@ 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.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; @@ -137,6 +138,7 @@ public class LoanProductData implements Serializable { private final Integer installmentAmountInMultiplesOf; private final EnumOptionData repaymentStartDateType; private final List supportedInterestRefundTypes; + private final StringEnumOptionData chargeOffBehaviour; // charges private final Collection charges; @@ -195,6 +197,7 @@ public class LoanProductData implements Serializable { private final List floatingRateOptions; private final List repaymentStartDateTypeOptions; private final List supportedInterestRefundTypesOptions; + private final List chargeOffBehaviourOptions; private final Boolean multiDisburseLoan; private final Integer maxTrancheCount; @@ -333,6 +336,7 @@ public static LoanProductData lookup(final Long id, final String name, final Boo final EnumOptionData loanScheduleProcessingTypeOptions = null; final boolean enableAccrualActivityPosting = false; final List supportedInterestRefundTypes = null; + final StringEnumOptionData chargeOffBehaviour = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -353,7 +357,7 @@ public static LoanProductData lookup(final Long id, final String name, final Boo fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, - loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); + loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour); } @@ -455,6 +459,7 @@ public static LoanProductData lookupWithCurrency(final Long id, final String nam final EnumOptionData loanScheduleProcessingType = null; final boolean enableAccrualActivityPosting = false; final List supportedInterestRefundTypes = null; + final StringEnumOptionData chargeOffBehaviour = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -475,7 +480,7 @@ public static LoanProductData lookupWithCurrency(final Long id, final String nam fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, - loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); + loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour); } @@ -584,6 +589,7 @@ public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { final EnumOptionData loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.asEnumOptionData(); final boolean enableAccrualActivityPosting = false; final List supportedInterestRefundTypes = null; + final StringEnumOptionData chargeOffBehaviour = LoanChargeOffBehaviour.REGULAR.getValueAsStringEnumOptionData(); return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -604,7 +610,7 @@ public static LoanProductData sensibleDefaultsForNewLoanProductCreation() { fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, - loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); + loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour); } @@ -707,6 +713,7 @@ public static LoanProductData loanProductWithFloatingRates(final Long id, final final EnumOptionData loanScheduleProcessingType = null; final boolean enableAccrualActivityPosting = false; final List supportedInterestRefundTypes = null; + final StringEnumOptionData chargeOffBehaviour = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -727,7 +734,7 @@ public static LoanProductData loanProductWithFloatingRates(final Long id, final fixedPrincipalPercentagePerInstallment, delinquencyBucketOptions, delinquencyBucket, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, - loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); + loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map accountingMappings, @@ -778,7 +785,8 @@ public LoanProductData(final Long id, final String name, final String shortName, final Collection paymentAllocation, final Collection creditAllocation, final EnumOptionData repaymentStartDateType, final boolean enableInstallmentLevelDelinquency, final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes) { + final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, + StringEnumOptionData chargeOffBehaviour) { this.id = id; this.name = name; this.shortName = shortName; @@ -918,6 +926,8 @@ public LoanProductData(final Long id, final String name, final String shortName, this.enableAccrualActivityPosting = enableAccrualActivityPosting; this.supportedInterestRefundTypes = supportedInterestRefundTypes; this.supportedInterestRefundTypesOptions = null; + this.chargeOffBehaviour = chargeOffBehaviour; + this.chargeOffBehaviourOptions = null; this.chargeOffReasonOptions = null; } @@ -941,7 +951,8 @@ public LoanProductData(final LoanProductData productData, final Collection advancedPaymentAllocationTypes, final List loanScheduleTypeOptions, final List loanScheduleProcessingTypeOptions, final List creditAllocationTransactionTypes, final List creditAllocationAllocationTypes, - final List supportedInterestRefundTypesOptions, final List chargeOffReasonOptions) { + final List supportedInterestRefundTypesOptions, + final List chargeOffBehaviourOptions, final List chargeOffReasonOptions) { this.id = productData.id; this.name = productData.name; @@ -1099,6 +1110,8 @@ public LoanProductData(final LoanProductData productData, final Collection supportedInterestRefundTypes) { + final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, + final LoanChargeOffBehaviour chargeOffBehaviour) { this.fund = fund; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; @@ -738,7 +749,7 @@ public LoanProduct(final Fund fund, final String transactionProcessingStrategyCo inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, - supportedInterestRefundTypes); + supportedInterestRefundTypes, chargeOffBehaviour); 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 d6790db782a..e65a78fca17 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 @@ -37,6 +37,7 @@ 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.LoanChargeOffBehaviour; 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; @@ -159,6 +160,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "supported_interest_refund_types") private List supportedInterestRefundTypes = List.of(); + @Column(name = "charge_off_behaviour") + @Enumerated(EnumType.STRING) + private LoanChargeOffBehaviour chargeOffBehaviour; + public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currency, final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, @@ -171,7 +176,8 @@ public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currenc final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes) { + final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, + final LoanChargeOffBehaviour chargeOffBehaviour) { return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, nominalAnnualInterestRate, interestMethod, interestCalculationPeriodMethod, allowPartialPeriodInterestCalcualtion, @@ -179,7 +185,8 @@ public static LoanProductRelatedDetail createFrom(final MonetaryCurrency currenc recurringMoratoriumOnPrincipalPeriods, graceOnInterestPayment, graceOnInterestCharged, amortizationMethod, inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, - loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); + loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, + chargeOffBehaviour); } protected LoanProductRelatedDetail() { @@ -198,7 +205,8 @@ public LoanProductRelatedDetail(final MonetaryCurrency currency, final BigDecima final boolean enableDownPayment, final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, - final boolean enableAccrualActivityPosting, List supportedInterestRefundTypes) { + final boolean enableAccrualActivityPosting, List supportedInterestRefundTypes, + final LoanChargeOffBehaviour chargeOffBehaviour) { this.currency = currency; this.principal = defaultPrincipal; this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; @@ -233,6 +241,7 @@ public LoanProductRelatedDetail(final MonetaryCurrency currency, final BigDecima this.loanScheduleProcessingType = loanScheduleProcessingType; this.enableAccrualActivityPosting = enableAccrualActivityPosting; this.supportedInterestRefundTypes = supportedInterestRefundTypes; + this.chargeOffBehaviour = chargeOffBehaviour; } private Integer defaultToNullIfZero(final Integer value) { diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml index e3b76c41780..098e59db5ac 100644 --- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml @@ -45,4 +45,5 @@ + diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1023_add_charge_off_behaviour.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1023_add_charge_off_behaviour.xml new file mode 100644 index 00000000000..239d3dd5e08 --- /dev/null +++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1023_add_charge_off_behaviour.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + 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 bf6a554f0a3..eb02ba9bf5e 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 @@ -528,7 +528,8 @@ private LoanApplicationTerms assembleLoanApplicationTermsFrom(final JsonElement disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, fixedLength, loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting(), - loanProduct.getLoanProductRelatedDetail().getSupportedInterestRefundTypes()); + loanProduct.getLoanProductRelatedDetail().getSupportedInterestRefundTypes(), + loanProduct.getLoanProductRelatedDetail().getChargeOffBehaviour()); } private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, 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 b2ac2594297..3c9351cc61e 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 @@ -79,6 +79,7 @@ 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.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; @@ -429,6 +430,7 @@ private LoanProductData handleTemplate(final LoanProductData productData) { final List creditAllocationAllocationTypes = AllocationType.getValuesAsEnumOptionDataList(); final List supportedInterestRefundTypesOptions = LoanSupportedInterestRefundTypes .getValuesAsStringEnumOptionDataList(); + final List chargeOffBehaviourOptions = LoanChargeOffBehaviour.getValuesAsStringEnumOptionDataList(); final List chargeOffReasonOptions = codeValueReadPlatformService .retrieveCodeValuesByCode(LoanApiConstants.CHARGE_OFF_REASONS); @@ -442,7 +444,7 @@ private LoanProductData handleTemplate(final LoanProductData productData) { advancedPaymentAllocationTransactionTypes, advancedPaymentAllocationFutureInstallmentAllocationRules, advancedPaymentAllocationTypes, LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes, - creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffReasonOptions); + creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions); } } 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..7ecc81e6f38 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,7 @@ 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.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; @@ -182,7 +183,8 @@ public final class LoanProductDataValidator { LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.REPAYMENT_START_DATE_TYPE, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, LoanProductConstants.LOAN_SCHEDULE_TYPE, LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH, - LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES)); + LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES, + LoanProductConstants.CHARGE_OFF_BEHAVIOUR)); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, @@ -862,6 +864,17 @@ public void validateForCreate(final JsonCommand command) { "Automatic interest refund functionality is only supported for Progressive loans"); } + if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(transactionProcessingStrategyCode) + && this.fromApiJsonHelper.parameterExists(LoanProductConstants.CHARGE_OFF_BEHAVIOUR, element)) { + String chargeOffBehaviour = this.fromApiJsonHelper.extractStringNamed(LoanProductConstants.CHARGE_OFF_BEHAVIOUR, element); + baseDataValidator.reset().parameter(LoanProductConstants.CHARGE_OFF_BEHAVIOUR).value(chargeOffBehaviour) + .isOneOfEnumValues(LoanChargeOffBehaviour.class); + } else if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.CHARGE_OFF_BEHAVIOUR, element)) { + baseDataValidator.reset().parameter(LoanProductConstants.CHARGE_OFF_BEHAVIOUR).failWithCode( + "supported.only.for.progressive.loan.charge.off.behaviour", + "Charge off behaviour is only supported for Progressive loans"); + } + throwExceptionIfValidationWarningsExist(dataValidationErrors); } 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..feddcf502cb 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 @@ -44,6 +44,7 @@ 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.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.data.AdvancedPaymentData; @@ -278,8 +279,9 @@ public String loanProductSchema() { + "lfr.is_floating_interest_rate_calculation_allowed as isFloatingInterestRateCalculationAllowed, " + "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 " - + " from m_product_loan lp " + " left join m_fund f on f.id = lp.fund_id " + + "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 " + " 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 " @@ -535,6 +537,8 @@ public LoanProductData mapRow(@NotNull final ResultSet rs, @SuppressWarnings("un .map(LoanSupportedInterestRefundTypes::valueOf) .map(LoanSupportedInterestRefundTypes::getValueAsStringEnumOptionData).toList(); } + final String chargeOffBehaviourStr = rs.getString("chargeOffBehaviour"); + final LoanChargeOffBehaviour loanChargeOffBehaviour = LoanChargeOffBehaviour.valueOf(chargeOffBehaviourStr); return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -557,7 +561,8 @@ public LoanProductData mapRow(@NotNull final ResultSet rs, @SuppressWarnings("un dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, advancedPaymentData, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData(), - fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes); + fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, + loanChargeOffBehaviour.getValueAsStringEnumOptionData()); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 67b543149fc..2f25dfb7424 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -51,6 +51,7 @@ import org.apache.fineract.portfolio.fund.domain.Fund; import org.apache.fineract.portfolio.fund.domain.FundRepository; import org.apache.fineract.portfolio.fund.exception.FundNotFoundException; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; @@ -277,6 +278,11 @@ public CommandProcessingResult updateLoanProduct(final Long loanProductId, final product.getLoanProductRelatedDetail().setSupportedInterestRefundTypes(supportedInterestRefundTypes); } + if (command.parameterExists(LoanProductConstants.CHARGE_OFF_BEHAVIOUR)) { + product.getLoanProductRelatedDetail().setChargeOffBehaviour( + command.enumValueOfParameterNamed(LoanProductConstants.CHARGE_OFF_BEHAVIOUR, LoanChargeOffBehaviour.class)); + } + if (!changes.isEmpty()) { product.validateLoanProductPreSave(); this.loanProductRepository.saveAndFlush(product); 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 4a212eee558..61cd464f937 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,7 @@ public void test_generateRepaymentPeriods() { 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); + submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null); // when List result = underTest.generateRepaymentPeriods(mathContext, expectedDisbursementDate, @@ -169,7 +169,7 @@ private LoanApplicationTerms createLoanApplicationTerms(LocalDate dueRepaymentPe null, null, null, null, null, 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); + DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null); } private HolidayDetailDTO createHolidayDTO() { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java index 40edf8ac3cc..5b83691d035 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanProductWithAdvancedPaymentAllocationIntegrationTests.java @@ -45,6 +45,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +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; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; @@ -408,6 +409,50 @@ public void testCreateShouldFailWhenNoInterestRateIsProvided() { loanProductError.get(0).get("defaultUserMessage").replace('`', ' ')); } + @Test + public void testCreateAndReadProgressiveLoanProductWithChargeOffBehaviour() { + // given + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); + AdvancedPaymentData repaymentPaymentAllocation = createRepaymentPaymentAllocation(); + + // when + String loanProductRequest = loanProductTestBuilder( + customization -> customization.addAdvancedPaymentAllocation(defaultAllocation, repaymentPaymentAllocation) + .withChargeOffBehaviour(LoanChargeOffBehaviour.ZERO_INTEREST)); + + Integer loanProductId = LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductRequest); + Assertions.assertNotNull(loanProductId); + GetLoanProductsProductIdResponse loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + + // then + Assertions.assertNotNull(loanProduct.getChargeOffBehaviour()); + Assertions.assertEquals(LoanChargeOffBehaviour.ZERO_INTEREST.name(), loanProduct.getChargeOffBehaviour().getId()); + + // when a new interest refund transaction was added + LOAN_TRANSACTION_HELPER.updateLoanProduct(loanProductId.longValue(), + new PutLoanProductsProductIdRequest().chargeOffBehaviour(LoanChargeOffBehaviour.REGULAR.name())); + + loanProduct = LOAN_TRANSACTION_HELPER.getLoanProduct(loanProductId); + + // then + Assertions.assertNotNull(loanProduct.getChargeOffBehaviour()); + Assertions.assertEquals(LoanChargeOffBehaviour.REGULAR.name(), loanProduct.getChargeOffBehaviour().getId()); + } + + @Test + public void testCreateCumulativeLoanProductWithChargeOff() { + // given + // when + String loanProductRequest = loanProductTestBuilder( + customization -> customization.withChargeOffBehaviour(LoanChargeOffBehaviour.ZERO_INTEREST) + .withLoanScheduleType(LoanScheduleType.CUMULATIVE).withRepaymentStrategy("mifos-standard-strategy")); + LoanTransactionHelper loanTransactionHelperBadRequest = new LoanTransactionHelper(REQUEST_SPEC, + new ResponseSpecBuilder().expectStatusCode(400).build()); + List> loanProductError = loanTransactionHelperBadRequest.getLoanProductError(loanProductRequest, "errors"); + Assertions.assertEquals("validation.msg.loanproduct.chargeOffBehaviour.supported.only.for.progressive.loan.charge.off.behaviour", + loanProductError.get(0).get("userMessageGlobalisationCode")); + } + private String loanProductTestBuilder(Consumer customization) { LoanProductTestBuilder builder = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4") .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("1") diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index 75624a8c598..eded00f83ca 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -31,6 +31,7 @@ import org.apache.fineract.client.models.CreditAllocationData; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.integrationtests.common.accounting.Account; +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; @@ -162,6 +163,7 @@ public class LoanProductTestBuilder { private String loanScheduleProcessingType = LoanScheduleProcessingType.HORIZONTAL.name(); private FullAccountingConfig fullAccountingConfig; private List supportedInterestRefundTypes = null; + private String chargeOffBehaviour; public String build() { final HashMap map = build(null, null); @@ -330,6 +332,10 @@ public HashMap build(final String chargeId, final Integer delinq map.put("supportedInterestRefundTypes", supportedInterestRefundTypes); } + if (this.chargeOffBehaviour != null) { + map.put("chargeOffBehaviour", chargeOffBehaviour); + } + return map; } @@ -801,6 +807,11 @@ public LoanProductTestBuilder withSupportedInterestRefundTypes(String... refundT return this; } + public LoanProductTestBuilder withChargeOffBehaviour(LoanChargeOffBehaviour chargeOffBehaviour) { + this.chargeOffBehaviour = chargeOffBehaviour.name(); + return this; + } + public LoanProductTestBuilder withChargeOffReasonsToExpenseMappings(final Long reasonId, final Long accountId) { if (this.chargeOffReasonsToExpenseMappings == null) { this.chargeOffReasonsToExpenseMappings = new ArrayList<>();