FINERACT-2354: Re-aging-for-interest-bearing-products

This commit is contained in:
Jose Alberto Hernandez
2025-10-15 11:38:10 +02:00
committed by Adam Saghy
parent c50113a868
commit b9e6f21e18
18 changed files with 258 additions and 10 deletions
@@ -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);
@@ -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() {
}
@@ -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";
@@ -28,4 +28,7 @@ public interface LoanReAgingApiConstants {
String frequencyNumber = "frequencyNumber";
String startDate = "startDate";
String numberOfInstallments = "numberOfInstallments";
String reAgeInterestHandlingParamName = "reAgeInterestHandling";
String reasonCodeValueIdParamName = "reasonCodeValueId";
}
@@ -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;
@@ -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)
@@ -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());
}
}
@@ -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);
}
}
@@ -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)
@@ -165,4 +165,5 @@ public interface LoanReadPlatformService {
Long getResolvedLoanTransactionId(Long transactionId, ExternalId externalTransactionId);
LoanTransactionData retrieveLoanReAgeTemplate(Long loanId);
}
@@ -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>
@@ -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>
@@ -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);
}
@@ -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) {
@@ -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) {
@@ -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);
}
@@ -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);
}
@@ -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));
});
}
}