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
The following commit(s) were added to refs/heads/develop by this push:
new b9e6f21e18 FINERACT-2354: Re-aging-for-interest-bearing-products
b9e6f21e18 is described below
commit b9e6f21e18c327533be5160e530de11da7ef7be6
Author: Jose Alberto Hernandez <[email protected]>
AuthorDate: Mon Sep 22 07:58:47 2025 -0500
FINERACT-2354: Re-aging-for-interest-bearing-products
---
.../core/data/DataValidatorBuilder.java | 2 +-
.../common/service/CommonEnumerations.java | 3 +
.../loanaccount/api/LoanApiConstants.java | 3 +
.../loanaccount/api/LoanReAgingApiConstants.java | 3 +
.../api/LoanTransactionsApiResourceSwagger.java | 5 ++
.../loanaccount/data/LoanTransactionData.java | 6 ++
.../reaging/LoanReAgeInterestHandlingType.java | 50 ++++++++++++++++
.../domain/reaging/LoanReAgeParameter.java | 13 ++++-
.../loanaccount/mapper/LoanTransactionMapper.java | 3 +
.../service/LoanReadPlatformService.java | 1 +
.../tenant/module/loan/module-changelog-master.xml | 1 +
.../loan/parts/1033_add_reage_reasons_for_loan.xml | 47 +++++++++++++++
.../api/LoanTransactionsApiResource.java | 2 +
.../service/LoanReadPlatformServiceImpl.java | 15 +++++
.../service/reaging/LoanReAgingServiceImpl.java | 23 +++++++-
.../service/reaging/LoanReAgingValidator.java | 20 +++++++
.../integrationtests/BaseLoanIntegrationTest.java | 4 +-
.../loan/reaging/LoanReAgingIntegrationTest.java | 67 ++++++++++++++++++++--
18 files changed, 258 insertions(+), 10 deletions(-)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
index c8745d7a36..1a8f1bedd1 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
@@ -990,7 +990,7 @@ public class DataValidatorBuilder {
final LocalDate dateVal = (LocalDate) this.value;
if (DateUtils.isAfter(date, dateVal)) {
String validationErrorCode = "validation.msg." + this.resource
+ "." + this.parameter + ".is.less.than.date";
- String defaultEnglishMessage = "The parameter `" +
this.parameter + "` must be greater than the provided date" + date;
+ String defaultEnglishMessage = "The parameter `" +
this.parameter + "` must be greater than the provided date " + date;
final ApiParameterError error =
ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage,
this.parameter,
dateVal, date);
this.dataValidationErrors.add(error);
diff --git
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/CommonEnumerations.java
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/CommonEnumerations.java
index 61a0b9380c..ba27aece22 100644
---
a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/CommonEnumerations.java
+++
b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/service/CommonEnumerations.java
@@ -28,6 +28,9 @@ import
org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
public final class CommonEnumerations {
+ public static final List<PeriodFrequencyType> BASIC_PERIOD_FREQUENCY_TYPES
= List.of(PeriodFrequencyType.DAYS,
+ PeriodFrequencyType.WEEKS, PeriodFrequencyType.MONTHS,
PeriodFrequencyType.YEARS);
+
private CommonEnumerations() {
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
index d06a0ee244..2bd8b800d8 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java
@@ -140,6 +140,8 @@ public interface LoanApiConstants {
String WRITEOFFREASONS = "WriteOffReasons";
// loan charge-off
String CHARGE_OFF_REASONS = "ChargeOffReasons";
+ // loan ReAge
+ String REAGE_REASONS = "ReAgeReasons";
// fore closure constants
String transactionDateParamName = "transactionDate";
String noteParamName = "note";
@@ -183,6 +185,7 @@ public interface LoanApiConstants {
String UNDO_CONTRACT_TERMINATION_COMMAND = "undoContractTermination";
String BUY_DOWN_FEE_COMMAND = "buyDownFee";
String BUY_DOWN_FEE_ADJUSTMENT_COMMAND = "buyDownFeeAdjustment";
+ String REAGE_COMMAND = "reAge";
// Data Validator names
String LOAN_FRAUD_DATAVALIDATOR_PREFIX = "loans.fraud";
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java
index 56c18c33a2..701cb65c1f 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java
@@ -28,4 +28,7 @@ public interface LoanReAgingApiConstants {
String frequencyNumber = "frequencyNumber";
String startDate = "startDate";
String numberOfInstallments = "numberOfInstallments";
+
+ String reAgeInterestHandlingParamName = "reAgeInterestHandling";
+ String reasonCodeValueIdParamName = "reasonCodeValueId";
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
index dd0bb22c1b..324d905e1f 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResourceSwagger.java
@@ -368,6 +368,11 @@ final class LoanTransactionsApiResourceSwagger {
public String startDate;
@Schema(example = "numberOfInstallments")
public Integer numberOfInstallments;
+ @Schema(example = "DEFAULT")
+ public String reAgeInterestHandling;
+ @Schema(example = "1")
+ public Long reasonCodeValueId;
+
// command=reAge END
@Schema(description = "Optional. Controls whether Interest Refund
transaction should be created for this refund. If not provided, loan product
config is used.", example = "false")
public Boolean interestRefundCalculation;
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
index c0a4ab510a..715af39a8a 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionData.java
@@ -30,9 +30,11 @@ import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
+import org.apache.fineract.infrastructure.core.data.StringEnumOptionData;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.account.data.AccountTransferData;
+import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
@@ -116,6 +118,10 @@ public class LoanTransactionData implements Serializable {
private Collection<CodeValueData> classificationOptions = null;
private CodeValueData classification;
+ private Collection<CodeValueData> reAgeReasonOptions = null;
+ private Collection<PeriodFrequencyType> periodFrequencyOptions = null;
+ private Collection<StringEnumOptionData> reAgeInterestHandlingOptions =
null;
+
public static LoanTransactionData importInstance(BigDecimal
repaymentAmount, LocalDate lastRepaymentDate, Long repaymentTypeId,
Integer rowIndex, String locale, String dateFormat) {
return
LoanTransactionData.builder().transactionAmount(repaymentAmount).transactionDate(lastRepaymentDate)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeInterestHandlingType.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeInterestHandlingType.java
new file mode 100644
index 0000000000..f61ba080a0
--- /dev/null
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeInterestHandlingType.java
@@ -0,0 +1,50 @@
+/**
+ * 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.reaging;
+
+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 LoanReAgeInterestHandlingType {
+
+ DEFAULT("loanReAgeInterestHandlingType.default", "Default"), //
+ WAIVE_INTEREST("loanReAgeInterestHandlingType.waiveInterest", "Waive
Interest"), //
+
EQUAL_AMORTIZATION_PAYABLE_INTEREST("loanReAgeInterestHandlingType.equalAmortizationPayableInterest",
+ "Equal Amortization of Outstanding payable Interest"), //
+
EQUAL_AMORTIZATION_FULL_INTEREST("loanReAgeInterestHandlingType.equalAmortizationFullInterest",
+ "Equal Amortization of Outstanding full Interest"), //
+ ;
+
+ private final String code;
+ private final String humanReadableName;
+
+ public static List<StringEnumOptionData> getValuesAsEnumOptionDataList() {
+ return Arrays.stream(values()).map(v -> new
StringEnumOptionData(v.name(), v.getCode(), v.getHumanReadableName())).toList();
+ }
+
+ public StringEnumOptionData asEnumOptionData() {
+ return new StringEnumOptionData(name(), getCode(),
getHumanReadableName());
+ }
+
+}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java
index 84b0ee3332..b9dcb87b28 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/reaging/LoanReAgeParameter.java
@@ -23,11 +23,13 @@ import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
+import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import
org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -56,10 +58,19 @@ public class LoanReAgeParameter extends
AbstractAuditableWithUTCDateTimeCustom<L
@Column(name = "number_of_installments", nullable = false)
private Integer numberOfInstallments;
+ @Enumerated(EnumType.STRING)
+ @Column(name = "interest_handling_type")
+ private LoanReAgeInterestHandlingType interestHandlingType;
+
+ @ManyToOne
+ @JoinColumn(name = "reage_reason_code_value_id", nullable = true)
+ private CodeValue reageReason;
+
// for JPA, don't use
protected LoanReAgeParameter() {}
public LoanReAgeParameter getCopy(LoanTransaction loanTransaction) {
- return new LoanReAgeParameter(loanTransaction, frequencyType,
frequencyNumber, startDate, numberOfInstallments);
+ return new LoanReAgeParameter(loanTransaction, frequencyType,
frequencyNumber, startDate, numberOfInstallments,
+ interestHandlingType, reageReason);
}
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java
index 57eb2498fc..4aa9a0451f 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanTransactionMapper.java
@@ -33,6 +33,9 @@ public interface LoanTransactionMapper {
@Mapping(target = "loanRepaymentScheduleInstallments", ignore = true)
@Mapping(target = "writeOffReasonOptions", ignore = true)
@Mapping(target = "chargeOffReasonOptions", ignore = true)
+ @Mapping(target = "reAgeReasonOptions", ignore = true)
+ @Mapping(target = "periodFrequencyOptions", ignore = true)
+ @Mapping(target = "reAgeInterestHandlingOptions", ignore = true)
@Mapping(target = "classificationOptions", ignore = true)
@Mapping(target = "paymentTypeOptions", ignore = true)
@Mapping(target = "overpaymentPortion", ignore = true)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
index 05cb824a21..29773944ab 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java
@@ -165,4 +165,5 @@ public interface LoanReadPlatformService {
Long getResolvedLoanTransactionId(Long transactionId, ExternalId
externalTransactionId);
+ LoanTransactionData retrieveLoanReAgeTemplate(Long loanId);
}
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 97127b644c..b319dc40e7 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
@@ -55,4 +55,5 @@
<include relativeToChangelogFile="true"
file="parts/1030_add_loan_undo_contract_termination_event.xml"/>
<include relativeToChangelogFile="true"
file="parts/1031_loan_merchant_buy_down_fee.xml"/>
<include relativeToChangelogFile="true"
file="parts/1032_add_classification_to_loan_transaction.xml"/>
+ <include relativeToChangelogFile="true"
file="parts/1033_add_reage_reasons_for_loan.xml"/>
</databaseChangeLog>
diff --git
a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1033_add_reage_reasons_for_loan.xml
b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1033_add_reage_reasons_for_loan.xml
new file mode 100644
index 0000000000..1a776ecb54
--- /dev/null
+++
b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1033_add_reage_reasons_for_loan.xml
@@ -0,0 +1,47 @@
+<?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">
+ <insert tableName="m_code">
+ <column name="code_name" value="ReAgeReasons"/>
+ <column name="is_system_defined" valueBoolean="true"/>
+ </insert>
+ </changeSet>
+ <changeSet author="fineract" id="2"
objectQuotingStrategy="QUOTE_ALL_OBJECTS">
+ <addColumn tableName="m_loan_reage_parameter">
+ <column defaultValue="DEFAULT" name="interest_handling_type"
type="VARCHAR(40)">
+ <constraints nullable="false"/>
+ </column>
+ </addColumn>
+ </changeSet>
+ <changeSet author="fineract" id="3">
+ <addColumn tableName="m_loan_reage_parameter">
+ <column defaultValueComputed="NULL"
name="reage_reason_code_value_id" type="INT"/>
+ </addColumn>
+ <addForeignKeyConstraint baseColumnNames="reage_reason_code_value_id"
baseTableName="m_loan_reage_parameter"
+
constraintName="FK_reage_reason_code_m_code_value" deferrable="false"
+ initiallyDeferred="false" onDelete="RESTRICT"
onUpdate="RESTRICT"
+ referencedColumnNames="id"
referencedTableName="m_code_value" validate="true"/>
+ </changeSet>
+</databaseChangeLog>
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
index f8c45fcc53..ac85e4b43c 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
@@ -697,6 +697,8 @@ public class LoanTransactionsApiResource {
LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT,
transactionId);
} else if (CommandParameterUtil.is(commandParam,
INTEREST_REFUND_COMMAND_VALUE)) {
transactionData =
this.loanReadPlatformService.retrieveManualInterestRefundTemplate(resolvedLoanId,
transactionId);
+ } else if (CommandParameterUtil.is(commandParam,
LoanApiConstants.REAGE_COMMAND)) {
+ transactionData =
this.loanReadPlatformService.retrieveLoanReAgeTemplate(resolvedLoanId);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
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 5c31a4d997..28d360460d 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
@@ -131,6 +131,7 @@ import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
import
org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import
org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
@@ -2102,6 +2103,20 @@ public class LoanReadPlatformServiceImpl implements
LoanReadPlatformService, Loa
return loanTransactionData;
}
+ @Override
+ public LoanTransactionData retrieveLoanReAgeTemplate(final Long loanId) {
+ final LoanAccountData loan = this.retrieveOne(loanId);
+ final LoanTransactionEnumData transactionType =
LoanEnumerations.transactionType(LoanTransactionType.REAGE);
+ final BigDecimal totalOutstanding = loan.getSummary() != null ?
loan.getSummary().getTotalOutstanding() : null;
+ final List<CodeValueData> reAgeReasonOptions = new ArrayList<>(
+
codeValueReadPlatformService.retrieveCodeValuesByCode(LoanApiConstants.REAGE_REASONS));
+ return
LoanTransactionData.builder().type(transactionType).currency(loan.getCurrency()).date(DateUtils.getBusinessLocalDate())
+
.amount(totalOutstanding).netDisbursalAmount(loan.getNetDisbursalAmount()).loanId(loanId)
+
.externalLoanId(loan.getExternalId()).periodFrequencyOptions(CommonEnumerations.BASIC_PERIOD_FREQUENCY_TYPES)
+ .reAgeReasonOptions(reAgeReasonOptions)
+
.reAgeInterestHandlingOptions(LoanReAgeInterestHandlingType.getValuesAsEnumOptionDataList()).build();
+ }
+
@Override
public LoanTransactionData retrieveLoanChargeOffTemplate(final Long
loanId) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
index ab5b9f0e72..a01487a3c4 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java
@@ -26,6 +26,8 @@ import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.codes.domain.CodeValue;
+import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import
org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
@@ -37,9 +39,9 @@ import
org.apache.fineract.infrastructure.event.business.domain.loan.reaging.Loa
import
org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent;
import
org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanUndoReAgeTransactionBusinessEvent;
import
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
-import
org.apache.fineract.infrastructure.event.business.service.TransactionHelper;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
+import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -47,6 +49,7 @@ import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTra
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
import
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
@@ -78,7 +81,7 @@ public class LoanReAgingServiceImpl {
private final LoanUtilService loanUtilService;
private final LoanScheduleService loanScheduleService;
private final ReprocessLoanTransactionsService
reprocessLoanTransactionsService;
- private final TransactionHelper transactionHelper;
+ private final CodeValueRepository codeValueRepository;
public CommandProcessingResult reAge(Long loanId, JsonCommand command) {
Loan loan = loanAssembler.assembleFrom(loanId);
@@ -196,7 +199,21 @@ public class LoanReAgingServiceImpl {
LocalDate startDate =
command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
Integer numberOfInstallments =
command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
Integer periodFrequencyNumber =
command.integerValueOfParameterNamed(LoanReAgingApiConstants.frequencyNumber);
- return new LoanReAgeParameter(reAgeTransaction, periodFrequencyType,
periodFrequencyNumber, startDate, numberOfInstallments);
+
+ LoanReAgeInterestHandlingType reAgeInterestHandlingType = command
+
.enumValueOfParameterNamed(LoanReAgingApiConstants.reAgeInterestHandlingParamName,
LoanReAgeInterestHandlingType.class);
+ if (reAgeInterestHandlingType == null) {
+ reAgeInterestHandlingType = LoanReAgeInterestHandlingType.DEFAULT;
+ }
+
+ CodeValue reasonCodeValue = null;
+ if
(command.parameterExists(LoanReAgingApiConstants.reasonCodeValueIdParamName)) {
+ reasonCodeValue =
codeValueRepository.findByCodeNameAndId(LoanApiConstants.REAGE_REASONS,
+
command.longValueOfParameterNamed(LoanReAgingApiConstants.reasonCodeValueIdParamName));
+ }
+
+ return new LoanReAgeParameter(reAgeTransaction, periodFrequencyType,
periodFrequencyNumber, startDate, numberOfInstallments,
+ reAgeInterestHandlingType, reasonCodeValue);
}
private void persistNote(Loan loan, JsonCommand command, Map<String,
Object> changes) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java
index dfefabf286..149eb93365 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java
@@ -26,17 +26,21 @@ import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.codes.domain.CodeValue;
+import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import
org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
+import
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChangeOperation;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
@@ -47,6 +51,7 @@ import org.springframework.stereotype.Component;
public class LoanReAgingValidator {
private final LoanTransactionRepository loanTransactionRepository;
+ private final CodeValueRepository codeValueRepository;
public void validateReAge(Loan loan, JsonCommand command) {
validateReAgeRequest(loan, command);
@@ -80,6 +85,21 @@ public class LoanReAgingValidator {
baseDataValidator.reset().parameter(LoanReAgingApiConstants.numberOfInstallments).value(numberOfInstallments).notNull()
.integerGreaterThanZero();
+ final LoanReAgeInterestHandlingType reAgeInterestHandlingType = command
+
.enumValueOfParameterNamed(LoanReAgingApiConstants.reAgeInterestHandlingParamName,
LoanReAgeInterestHandlingType.class);
+
baseDataValidator.reset().parameter(LoanReAgingApiConstants.reAgeInterestHandlingParamName).value(reAgeInterestHandlingType)
+ .ignoreIfNull();
+
+ Long reasonCodeValueId =
command.longValueOfParameterNamed(LoanReAgingApiConstants.reasonCodeValueIdParamName);
+
baseDataValidator.reset().parameter(LoanReAgingApiConstants.reasonCodeValueIdParamName).value(reasonCodeValueId).ignoreIfNull();
+ if (reasonCodeValueId != null) {
+ final CodeValue reasonCodeValue =
codeValueRepository.findByCodeNameAndId(LoanApiConstants.REAGE_REASONS,
reasonCodeValueId);
+ if (reasonCodeValue == null) {
+
dataValidationErrors.add(ApiParameterError.parameterError("validation.msg.reage.reason.invalid",
+ "Reage Reason with ID " + reasonCodeValueId + " does
not exist", LoanApiConstants.REAGE_REASONS));
+ }
+ }
+
throwExceptionIfValidationErrorsExist(dataValidationErrors);
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index ba07af2d60..45280c101f 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -1013,7 +1013,8 @@ public abstract class BaseLoanIntegrationTest extends
IntegrationTest {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
}
- protected void reAgeLoan(Long loanId, String frequencyType, int
frequencyNumber, String startDate, Integer numberOfInstallments) {
+ protected void reAgeLoan(Long loanId, String frequencyType, int
frequencyNumber, String startDate, Integer numberOfInstallments,
+ String reAgeInterestHandling) {
PostLoansLoanIdTransactionsRequest request = new
PostLoansLoanIdTransactionsRequest();
request.setDateFormat(DATETIME_PATTERN);
request.setLocale("en");
@@ -1021,6 +1022,7 @@ public abstract class BaseLoanIntegrationTest extends
IntegrationTest {
request.setFrequencyNumber(frequencyNumber);
request.setStartDate(startDate);
request.setNumberOfInstallments(numberOfInstallments);
+ request.setReAgeInterestHandling(reAgeInterestHandling);
loanTransactionHelper.reAge(loanId, request);
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
index 199c5702f9..bb62309d37 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java
@@ -41,6 +41,7 @@ import
org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
import org.apache.fineract.integrationtests.common.ClientHelper;
import
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import
org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
import org.junit.jupiter.api.Test;
public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
@@ -135,7 +136,7 @@ public class LoanReAgingIntegrationTest extends
BaseLoanIntegrationTest {
long loanId = createdLoanId.get();
// create re-age transaction
- reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12
April 2023", 4);
+ reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12
April 2023", 4, null);
// verify transactions
verifyTransactions(loanId, //
@@ -209,7 +210,7 @@ public class LoanReAgingIntegrationTest extends
BaseLoanIntegrationTest {
);
// create re-age transaction
- reAgeLoan(loanId, RepaymentFrequencyType.DAYS_STRING, 30, "13
April 2023", 3);
+ reAgeLoan(loanId, RepaymentFrequencyType.DAYS_STRING, 30, "13
April 2023", 3, null);
// verify transactions
verifyTransactions(loanId, //
@@ -387,7 +388,7 @@ public class LoanReAgingIntegrationTest extends
BaseLoanIntegrationTest {
long loanId = createdLoanId.get();
// create re-age transaction
- reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12
April 2023", 4);
+ reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12
April 2023", 4, null);
// verify transactions
verifyTransactions(loanId, //
@@ -481,7 +482,7 @@ public class LoanReAgingIntegrationTest extends
BaseLoanIntegrationTest {
long loanId = createdLoanId.get();
// create re-age transaction
- reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "01
March 2023", 6);
+ reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "01
March 2023", 6, null);
// verify transactions
verifyTransactions(loanId, //
@@ -630,4 +631,62 @@ public class LoanReAgingIntegrationTest extends
BaseLoanIntegrationTest {
assertTrue(exception.getMessage().contains("error.msg.loan.transaction.not.found"));
});
}
+
+ @Test
+ public void test_LoanReAgeTransactionWithInterestHandling() {
+ AtomicLong createdLoanId = new AtomicLong();
+
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ int numberOfRepayments = 3;
+ int repaymentEvery = 1;
+
+ // Create Loan Product
+ PostLoanProductsRequest product =
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
//
+ .numberOfRepayments(numberOfRepayments) //
+ .repaymentEvery(repaymentEvery) //
+ .installmentAmountInMultiplesOf(null) //
+ .enableDownPayment(true) //
+
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) //
+ .enableAutoRepaymentForDownPayment(true) //
+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
+
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(product);
+ Long loanProductId = loanProductResponse.getResourceId();
+
+ // Apply and Approve Loan
+ double amount = 1250.0;
+
+ PostLoansRequest applicationRequest = applyLoanRequest(clientId,
loanProductId, "01 January 2023", amount, numberOfRepayments)//
+
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
+ .repaymentEvery(repaymentEvery)//
+ .loanTermFrequency(numberOfRepayments)//
+ .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+ .loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
+
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(applicationRequest);
+
+ PostLoansLoanIdResponse approvedLoanResult =
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+ approveLoanRequest(amount, "01 January 2023"));
+
+ Long loanId = approvedLoanResult.getLoanId();
+
+ // disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January
2023");
+ createdLoanId.set(loanId);
+ });
+
+ runAt("12 April 2023", () -> {
+ long loanId = createdLoanId.get();
+
+ // create re-age transaction with Equal Amortization
+ reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12
April 2023", 4,
+
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name());
+
+ checkMaturityDates(loanId, LocalDate.of(2023, 7, 12),
LocalDate.of(2023, 7, 12));
+ });
+ }
+
}