FINERACT-2354: First step - basic implementation of re-aging for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change

This commit is contained in:
mariiaKraievska
2025-10-27 14:22:46 +01:00
committed by Adam Saghy
parent 23c1b2b8af
commit d21a1a98a2
17 changed files with 416 additions and 461 deletions
@@ -860,7 +860,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
loan.getLoanTransactions().add(refundTransaction);
@@ -278,7 +278,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (reprocessRequired) {
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
@@ -857,7 +858,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
if (reprocessRequired) {
addInstallmentIfPenaltyAppliedAfterLastDueDate(loan, lastChargeDate);
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
@@ -888,7 +890,8 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
loan.addLoanTransaction(loanChargeAdjustmentTransaction);
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
@@ -1446,7 +1449,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
&& DateUtils.isBefore(loanCharge.getDueLocalDate(), businessDate)) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
// Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule
@@ -994,7 +994,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
@@ -1304,6 +1304,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
validateLoanTransactionAmountChargeBack(loanTransaction, newTransaction);
if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) {
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
}
// Store the Loan Transaction Relation
LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction,
LoanTransactionRelationTypeEnum.CHARGEBACK);
@@ -2273,11 +2282,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
updateDisbursementDateAndAmountForTranche(loan, loanDisbursementDetails, command, changes, scheduleGeneratorDTO);
} else {
loan.getLoanProductRelatedDetail().setPrincipal(loan.getPrincipalAmountForRepaymentSchedule());
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
reprocessLoanTransactionsService.processPostDisbursementTransactions(loan);
}
@@ -2432,7 +2441,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
}
@@ -3000,7 +3010,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
loan.addLoanTransaction(interestRefundTxn);
@@ -3264,7 +3275,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
reprocessLoanTransactionsService.reprocessTransactions(loan);
@@ -3373,7 +3385,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
@@ -3430,7 +3442,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
writeOffTransaction.reverse();
loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, loan);
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
@@ -81,9 +81,8 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
LocalDate relatedRefundTransactionDate, List<LoanTransaction> transactionsToReprocess) {
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>(
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
@@ -165,8 +165,13 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
ProgressiveLoanInterestScheduleModel model = savedModel
.orElseGet(() -> advancedProcessor.calculateInterestScheduleModel(loan.getId(), loanTransaction.getTransactionDate()));
transactionCtx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), model);
final ProgressiveTransactionCtx progressiveTransactionCtx = new ProgressiveTransactionCtx(loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()),
new ChangedTransactionDetail(), model);
progressiveTransactionCtx.setChargedOff(loan.isChargedOff());
progressiveTransactionCtx.setWrittenOff(loan.isClosedWrittenOff());
progressiveTransactionCtx.setContractTerminated(loan.isContractTermination());
transactionCtx = progressiveTransactionCtx;
} else {
transactionCtx = new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail());
@@ -24,6 +24,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
@@ -95,16 +96,25 @@ public class LoanReAgingServiceImpl {
LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command);
reAgeTransaction.setLoanReAgeParameter(reAgeParameter);
loanTransactionRepository.saveAndFlush(reAgeTransaction);
if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy());
if (reAgeTransaction.getTransactionDate().isBefore(reAgeTransaction.getSubmittedOnDate())
&& !loan.isInterestBearingAndInterestRecalculationEnabled()) {
reprocessLoanTransactionsService.reprocessTransactionsWithPostTransactionChecks(loan, reAgeTransaction.getTransactionDate());
} else if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction()
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
final List<LoanTransaction> loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
loanTransactions.add(reAgeTransaction);
reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions);
} else {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy());
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
}
loan.updateLoanScheduleDependentDerivedFields();
persistNote(loan, command, changes);
@@ -135,13 +145,13 @@ public class LoanReAgingServiceImpl {
if (reAgeTransaction == null) {
throw new LoanTransactionNotFoundException("Re-Age transaction for loan was not found");
}
reverseReAgeTransaction(reAgeTransaction, command);
loanTransactionRepository.saveAndFlush(reAgeTransaction);
if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
|| loan.hasContractTerminationTransaction())) {
|| loan.hasContractTerminationTransaction() || (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null);
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
}
reverseReAgeTransaction(reAgeTransaction, command);
loanTransactionRepository.saveAndFlush(reAgeTransaction);
reprocessLoanTransactionsService.reprocessTransactions(loan);
loan.updateLoanScheduleDependentDerivedFields();
persistNote(loan, command, changes);
@@ -33,7 +33,6 @@ 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;
@@ -68,7 +67,7 @@ public class LoanReAgingValidator {
.notExceedingLengthOf(100);
LocalDate startDate = command.localDateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
if (loan.isProgressiveSchedule() && !loan.isInterestBearing()) {
if (loan.isProgressiveSchedule()) {
baseDataValidator.reset().parameter(LoanReAgingApiConstants.startDate).value(startDate).notNull()
.validateDateAfterOrEqual(loan.getDisbursementDate());
} else {
@@ -105,14 +104,6 @@ public class LoanReAgingValidator {
}
private void validateReAgeBusinessRules(Loan loan) {
// validate reaging shouldn't happen before maturity
// on progressive loans it can
if (!(loan.isProgressiveSchedule() && !loan.isInterestBearing())
&& DateUtils.isBefore(getBusinessLocalDate(), loan.getMaturityDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.cannot.be.submitted.before.maturity",
"Loan cannot be re-aged before maturity", loan.getId());
}
// validate reaging is only available for progressive schedule & advanced payment allocation
LoanScheduleType loanScheduleType = LoanScheduleType.valueOf(loan.getLoanProductRelatedDetail().getLoanScheduleType().name());
boolean isProgressiveSchedule = LoanScheduleType.PROGRESSIVE.equals(loanScheduleType);
@@ -127,12 +118,6 @@ public class LoanReAgingValidator {
loan.getId());
}
// validate reaging is only available for non-interest bearing loans
if (loan.isInterestBearing()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.supported.only.for.non.interest.loans",
"Loan reaging is only available for non-interest bearing loans", loan.getId());
}
// validate reaging is only done on an active loan
if (!loan.getStatus().isActive()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.supported.only.for.active.loans",
@@ -288,20 +288,6 @@ class LoanReAgingValidatorTest {
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero");
}
@Test
public void testValidateReAge_ShouldThrowException_WhenLoanIsBeforeMaturity() {
// given
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
Loan loan = loan();
JsonCommand command = jsonCommand();
// when
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
() -> underTest.validateReAge(loan, command));
// then
assertThat(result).isNotNull();
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reage.cannot.be.submitted.before.maturity");
}
@Test
public void testValidateReAge_ShouldThrowException_WhenStartDateIsBeforeMaturity() {
// given
@@ -350,20 +336,6 @@ class LoanReAgingValidatorTest {
.isEqualTo("error.msg.loan.reage.supported.only.for.progressive.loan.schedule.type");
}
@Test
public void testValidateReAge_ShouldThrowException_WhenLoanIsInterestBearing() {
// given
Loan loan = loan();
given(loan.isInterestBearing()).willReturn(true);
JsonCommand command = jsonCommand();
// when
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
() -> underTest.validateReAge(loan, command));
// then
assertThat(result).isNotNull();
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reage.supported.only.for.non.interest.loans");
}
@Test
public void testValidateReAge_ShouldThrowException_WhenLoanIsNotActive() {
// given