FINERACT-2398: Re-amortization Interest Handling configuration

This commit is contained in:
Jose Alberto Hernandez
2025-10-29 12:13:05 +01:00
committed by Adam Saghy
parent 8a86759773
commit a849e9bae3
22 changed files with 294 additions and 35 deletions
@@ -120,6 +120,7 @@
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmortizationParameter</class>
<class>org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepaymentScheduleHistory</class>
<class>org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest</class>
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProduct</class>
@@ -142,6 +142,8 @@ public interface LoanApiConstants {
String CHARGE_OFF_REASONS = "ChargeOffReasons";
// loan ReAge
String REAGE_REASONS = "ReAgeReasons";
// loan ReAmortization
String REAMORTIZATION_REASONS = "ReAmortizationReasons";
// fore closure constants
String transactionDateParamName = "transactionDate";
String noteParamName = "note";
@@ -186,6 +188,7 @@ public interface LoanApiConstants {
String BUY_DOWN_FEE_COMMAND = "buyDownFee";
String BUY_DOWN_FEE_ADJUSTMENT_COMMAND = "buyDownFeeAdjustment";
String REAGE_COMMAND = "reAge";
String REAMORTIZATION_COMMAND = "reAmortization";
// Data Validator names
String LOAN_FRAUD_DATAVALIDATOR_PREFIX = "loans.fraud";
@@ -23,4 +23,7 @@ public interface LoanReAmortizationApiConstants {
String localeParameterName = "locale";
String dateFormatParameterName = "dateFormat";
String externalIdParameterName = "externalId";
String reAmortizationInterestHandlingParamName = "reAmortizationInterestHandling";
String reasonCodeValueIdParamName = "reasonCodeValueId";
}
@@ -370,6 +370,8 @@ final class LoanTransactionsApiResourceSwagger {
public Integer numberOfInstallments;
@Schema(example = "DEFAULT")
public String reAgeInterestHandling;
@Schema(example = "DEFAULT")
public String reAmortizationInterestHandling;
@Schema(example = "1")
public Long reasonCodeValueId;
@@ -121,6 +121,8 @@ public class LoanTransactionData implements Serializable {
private Collection<CodeValueData> reAgeReasonOptions = null;
private Collection<PeriodFrequencyType> periodFrequencyOptions = null;
private Collection<StringEnumOptionData> reAgeInterestHandlingOptions = null;
private Collection<CodeValueData> reAmortizationReasonOptions = null;
private Collection<StringEnumOptionData> reAmortizationInterestHandlingOptions = null;
public static LoanTransactionData importInstance(BigDecimal repaymentAmount, LocalDate lastRepaymentDate, Long repaymentTypeId,
Integer rowIndex, String locale, String dateFormat) {
@@ -48,6 +48,7 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmortizationParameter;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
@@ -145,6 +146,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
private LoanReAgeParameter loanReAgeParameter;
@Setter
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
private LoanReAmortizationParameter loanReAmortizationParameter;
@Setter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "classification_cv_id")
@@ -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.reamortization;
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 LoanReAmortizationInterestHandlingType {
DEFAULT("loanReAmortizationInterestHandlingType.default", "Default"), //
WAIVE_INTEREST("loanReAmortizationInterestHandlingType.waiveInterest", "Waive Interest"), //
EQUAL_AMORTIZATION_INTEREST_SPLIT("loanReAmortizationInterestHandlingType.equalAmortizationInterestSplit",
"Equal Amortization Interest Split"), ///
;
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());
}
}
@@ -0,0 +1,55 @@
/**
* 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.reamortization;
import jakarta.persistence.Column;
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 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.loanaccount.domain.LoanTransaction;
@Entity
@Table(name = "m_loan_reamortization_parameter")
@AllArgsConstructor
@Getter
public class LoanReAmortizationParameter extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@OneToOne
@JoinColumn(name = "loan_transaction_id", nullable = false)
private LoanTransaction loanTransaction;
@Enumerated(EnumType.STRING)
@Column(name = "interest_handling_type")
private LoanReAmortizationInterestHandlingType interestHandlingType;
@ManyToOne
@JoinColumn(name = "reamortization_reason_code_value_id", nullable = true)
private CodeValue reamortizationReason;
// for JPA, don't use
protected LoanReAmortizationParameter() {}
}
@@ -34,8 +34,10 @@ public interface LoanTransactionMapper {
@Mapping(target = "writeOffReasonOptions", ignore = true)
@Mapping(target = "chargeOffReasonOptions", ignore = true)
@Mapping(target = "reAgeReasonOptions", ignore = true)
@Mapping(target = "reAmortizationReasonOptions", ignore = true)
@Mapping(target = "periodFrequencyOptions", ignore = true)
@Mapping(target = "reAgeInterestHandlingOptions", ignore = true)
@Mapping(target = "reAmortizationInterestHandlingOptions", ignore = true)
@Mapping(target = "classificationOptions", ignore = true)
@Mapping(target = "paymentTypeOptions", ignore = true)
@Mapping(target = "overpaymentPortion", ignore = true)
@@ -166,4 +166,6 @@ public interface LoanReadPlatformService {
Long getResolvedLoanTransactionId(Long transactionId, ExternalId externalTransactionId);
LoanTransactionData retrieveLoanReAgeTemplate(Long loanId);
LoanTransactionData retrieveLoanReAmortizationTemplate(Long loanId);
}
@@ -56,4 +56,5 @@
<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"/>
<include relativeToChangelogFile="true" file="parts/1034_loan_reamortization_parameters.xml"/>
</databaseChangeLog>
@@ -0,0 +1,77 @@
<?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.1.xsd">
<changeSet author="fineract" id="1" objectQuotingStrategy="QUOTE_ALL_OBJECTS">
<createTable tableName="m_loan_reamortization_parameter">
<column autoIncrement="true" name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="loan_transaction_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column defaultValue="DEFAULT" name="interest_handling_type" type="VARCHAR(40)">
<constraints nullable="false"/>
</column>
<column defaultValueComputed="NULL" name="reamortization_reason_code_value_id" type="INT"/>
<column name="created_by" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="last_modified_by" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet id="2-mysql" author="fineract" context="mysql">
<addColumn tableName="m_loan_reamortization_parameter">
<column name="created_on_utc" type="DATETIME(6)">
<constraints nullable="false"/>
</column>
<column name="last_modified_on_utc" type="DATETIME(6)">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="2-postgresql" author="fineract" context="postgresql">
<addColumn tableName="m_loan_reamortization_parameter">
<column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
<constraints nullable="false"/>
</column>
<column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="4">
<insert tableName="m_code">
<column name="code_name" value="ReAmortizationReasons"/>
<column name="is_system_defined" valueBoolean="true"/>
</insert>
</changeSet>
<changeSet author="fineract" id="5">
<addForeignKeyConstraint baseColumnNames="reamortization_reason_code_value_id" baseTableName="m_loan_reamortization_parameter"
constraintName="FK_reamortization_reason_code_m_code_value" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="m_code_value" validate="true"/>
</changeSet>
</databaseChangeLog>
@@ -109,6 +109,7 @@
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmortizationParameter</class>
<class>org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepaymentScheduleHistory</class>
<class>org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest</class>
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProduct</class>
@@ -108,6 +108,7 @@
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmortizationParameter</class>
<class>org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepaymentScheduleHistory</class>
<class>org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest</class>
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProduct</class>
@@ -699,6 +699,8 @@ public class LoanTransactionsApiResource {
transactionData = this.loanReadPlatformService.retrieveManualInterestRefundTemplate(resolvedLoanId, transactionId);
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.REAGE_COMMAND)) {
transactionData = this.loanReadPlatformService.retrieveLoanReAgeTemplate(resolvedLoanId);
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.REAMORTIZATION_COMMAND)) {
transactionData = this.loanReadPlatformService.retrieveLoanReAmortizationTemplate(resolvedLoanId);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
@@ -132,6 +132,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepayment
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.reamortization.LoanReAmortizationInterestHandlingType;
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;
@@ -2117,6 +2118,19 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
.reAgeInterestHandlingOptions(LoanReAgeInterestHandlingType.getValuesAsEnumOptionDataList()).build();
}
@Override
public LoanTransactionData retrieveLoanReAmortizationTemplate(final Long loanId) {
final LoanAccountData loan = this.retrieveOne(loanId);
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REAMORTIZE);
final BigDecimal totalOutstanding = loan.getSummary() != null ? loan.getSummary().getTotalOutstanding() : null;
final List<CodeValueData> reAmortizationReasonOptions = new ArrayList<>(
codeValueReadPlatformService.retrieveCodeValuesByCode(LoanApiConstants.REAMORTIZATION_REASONS));
return LoanTransactionData.builder().type(transactionType).currency(loan.getCurrency()).date(DateUtils.getBusinessLocalDate())
.amount(totalOutstanding).netDisbursalAmount(loan.getNetDisbursalAmount()).loanId(loanId)
.externalLoanId(loan.getExternalId()).reAmortizationReasonOptions(reAmortizationReasonOptions)
.reAmortizationInterestHandlingOptions(LoanReAmortizationInterestHandlingType.getValuesAsEnumOptionDataList()).build();
}
@Override
public LoanTransactionData retrieveLoanChargeOffTemplate(final Long loanId) {
@@ -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;
@@ -38,12 +40,15 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanUndoReAmortizeTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.api.LoanReAmortizationApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
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.reamortization.LoanReAmortizationInterestHandlingType;
import org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmortizationParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
@@ -66,6 +71,7 @@ public class LoanReAmortizationServiceImpl {
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final LoanChargeValidator loanChargeValidator;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final CodeValueRepository codeValueRepository;
public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) {
Loan loan = loanAssembler.assembleFrom(loanId);
@@ -82,6 +88,8 @@ public class LoanReAmortizationServiceImpl {
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAmortizeTransaction, new TransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
reAmortizeTransaction.setLoanReAmortizationParameter(createReAmortizationParameter(reAmortizeTransaction, command));
loanTransactionRepository.saveAndFlush(reAmortizeTransaction);
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
@@ -159,4 +167,20 @@ public class LoanReAmortizationServiceImpl {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAMORTIZE, transactionDate, txPrincipalAmount,
txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId);
}
private LoanReAmortizationParameter createReAmortizationParameter(LoanTransaction reAmortizationTransaction, JsonCommand command) {
LoanReAmortizationInterestHandlingType reAmortizationInterestHandlingType = command.enumValueOfParameterNamed(
LoanReAmortizationApiConstants.reAmortizationInterestHandlingParamName, LoanReAmortizationInterestHandlingType.class);
if (reAmortizationInterestHandlingType == null) {
reAmortizationInterestHandlingType = LoanReAmortizationInterestHandlingType.DEFAULT;
}
CodeValue reasonCodeValue = null;
if (command.parameterExists(LoanReAmortizationApiConstants.reasonCodeValueIdParamName)) {
reasonCodeValue = codeValueRepository.findByCodeNameAndId(LoanApiConstants.REAMORTIZATION_REASONS,
command.longValueOfParameterNamed(LoanReAmortizationApiConstants.reasonCodeValueIdParamName));
}
return new LoanReAmortizationParameter(reAmortizationTransaction, reAmortizationInterestHandlingType, reasonCodeValue);
}
}
@@ -24,23 +24,31 @@ import java.util.ArrayList;
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.LoanReAmortizationApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmortizationInterestHandlingType;
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;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class LoanReAmortizationValidator {
private final CodeValueRepository codeValueRepository;
public void validateReAmortize(Loan loan, JsonCommand command) {
validateReAmortizeRequest(command);
validateReAmortizeBusinessRules(loan);
@@ -54,6 +62,23 @@ public class LoanReAmortizationValidator {
baseDataValidator.reset().parameter(LoanReAmortizationApiConstants.externalIdParameterName).ignoreIfNull().value(externalId)
.notExceedingLengthOf(100);
final LoanReAmortizationInterestHandlingType reAmortizationInterestHandlingType = command.enumValueOfParameterNamed(
LoanReAmortizationApiConstants.reAmortizationInterestHandlingParamName, LoanReAmortizationInterestHandlingType.class);
baseDataValidator.reset().parameter(LoanReAmortizationApiConstants.reAmortizationInterestHandlingParamName)
.value(reAmortizationInterestHandlingType).ignoreIfNull();
Long reasonCodeValueId = command.longValueOfParameterNamed(LoanReAmortizationApiConstants.reasonCodeValueIdParamName);
baseDataValidator.reset().parameter(LoanReAmortizationApiConstants.reasonCodeValueIdParamName).value(reasonCodeValueId)
.ignoreIfNull();
if (reasonCodeValueId != null) {
final CodeValue reasonCodeValue = codeValueRepository.findByCodeNameAndId(LoanApiConstants.REAMORTIZATION_REASONS,
reasonCodeValueId);
if (reasonCodeValue == null) {
dataValidationErrors.add(ApiParameterError.parameterError("validation.msg.reamortization.reason.invalid",
"Reamortization Reason with ID " + reasonCodeValueId + " does not exist", LoanApiConstants.REAMORTIZATION_REASONS));
}
}
throwExceptionIfValidationErrorsExist(dataValidationErrors);
}
@@ -78,12 +103,6 @@ public class LoanReAmortizationValidator {
loan.getId());
}
// validate reamortization is only available for non-interest bearing loans
if (loan.isInterestBearing()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reamortize.supported.only.for.non.interest.loans",
"Loan reamortization is only available for non-interest bearing loans", loan.getId());
}
// validate reamortization is only done on an active loan
if (!loan.getStatus().isActive()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reamortize.supported.only.for.active.loans",
@@ -207,6 +207,7 @@
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter</class>
<class>org.apache.fineract.portfolio.loanaccount.domain.reamortization.LoanReAmortizationParameter</class>
<class>org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanRepaymentScheduleHistory</class>
<class>org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest</class>
<class>org.apache.fineract.portfolio.loanproduct.domain.LoanProduct</class>
@@ -53,13 +53,21 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@SuppressFBWarnings({ "VA_FORMAT_STRING_USES_NEWLINE" })
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class LoanReAmortizationValidatorTest {
private final LocalDate actualDate = LocalDate.now(Clock.systemUTC());
private LoanReAmortizationValidator underTest = new LoanReAmortizationValidator();
@InjectMocks
private LoanReAmortizationValidator underTest;
@BeforeEach
public void setUp() {
@@ -140,20 +148,6 @@ class LoanReAmortizationValidatorTest {
.isEqualTo("error.msg.loan.reamortize.supported.only.for.progressive.loan.schedule.type");
}
@Test
public void testValidateReAmortize_ShouldThrowException_WhenLoanIsInterestBearing() {
// given
Loan loan = loan();
given(loan.isInterestBearing()).willReturn(true);
JsonCommand command = jsonCommand();
// when
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
() -> underTest.validateReAmortize(loan, command));
// then
assertThat(result).isNotNull();
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reamortize.supported.only.for.non.interest.loans");
}
@Test
public void testValidateReAmortize_ShouldThrowException_WhenLoanIsNotActive() {
// given
@@ -1027,8 +1027,9 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
loanTransactionHelper.reAge(loanId, request);
}
protected void reAmortizeLoan(Long loanId) {
protected void reAmortizeLoan(Long loanId, String reAmortizationInterestHandling) {
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
request.setReAmortizationInterestHandling(reAmortizationInterestHandling);
request.setDateFormat(DATETIME_PATTERN);
request.setLocale("en");
loanTransactionHelper.reAmortize(loanId, request);
@@ -30,6 +30,7 @@ import org.apache.fineract.client.models.PostLoansResponse;
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.reamortization.LoanReAmortizationInterestHandlingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.junit.jupiter.api.Test;
@@ -90,7 +91,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
runAt("02 February 2023", () -> {
// create re-amortize transaction
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
// verify transactions
verifyTransactions(loanId.get(), //
@@ -157,7 +158,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
runAt("02 February 2023", () -> {
// create re-amortize transaction
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
// verify transactions
verifyTransactions(loanId.get(), //
@@ -220,7 +221,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
);
});
runAt("25 January 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -265,7 +266,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
});
runAt("01 February 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -319,7 +320,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
);
});
runAt("30 January 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -375,7 +376,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
);
});
runAt("31 January 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -421,7 +422,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
runAt("01 February 2023", () -> {
addCharge(loanId.get(), false, 10.0, "27 February 2023");
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -466,7 +467,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
);
});
runAt("01 February 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -549,7 +550,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
);
});
runAt("01 February 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -613,7 +614,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
});
runAt("01 February 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -677,7 +678,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
);
});
runAt("17 January 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -726,7 +727,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
});
runAt("25 January 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //
@@ -821,7 +822,7 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
});
runAt("25 January 2023", () -> {
reAmortizeLoan(loanId.get());
reAmortizeLoan(loanId.get(), LoanReAmortizationInterestHandlingType.DEFAULT.name());
verifyRepaymentSchedule(loanId.get(), //
installment(500, null, "01 January 2023"), //