FINERACT-2413: Improve re-amortization undo validation

This commit is contained in:
Oleksii Novikov
2025-12-23 11:12:41 +01:00
committed by Arnold Gálovics
parent b457b2aa93
commit 2858316c71
5 changed files with 182 additions and 79 deletions
@@ -115,16 +115,12 @@ public class LoanReAmortizationService {
public CommandProcessingResult undoReAmortize(Long loanId, JsonCommand command) {
Loan loan = loanAssembler.assembleFrom(loanId);
reAmortizationValidator.validateUndoReAmortize(loan, command);
final LoanTransaction reAmortizeTransaction = reAmortizationValidator.findAndValidateReAmortizeTransactionForUndo(loan);
Map<String, Object> changes = new LinkedHashMap<>();
changes.put(LoanReAmortizationApiConstants.localeParameterName, command.locale());
changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat());
LoanTransaction reAmortizeTransaction = findLatestNonReversedReAmortizeTransaction(loan);
if (reAmortizeTransaction == null) {
// TODO: when validations implemented; throw exception if there isn't a reamortize transaction available
}
if (loan.isProgressiveSchedule()) {
loanScheduleService.regenerateRepaymentSchedule(loan);
}
@@ -185,14 +181,6 @@ public class LoanReAmortizationService {
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan loan) {
return loan.getLoanTransactions().stream() //
.filter(LoanTransaction::isNotReversed) //
.filter(LoanTransaction::isReAmortize) //
.max(Comparator.comparing(LoanTransaction::getTransactionDate)) //
.orElse(null);
}
private LoanTransaction createReAmortizeTransaction(Loan loan, JsonCommand command) {
ExternalId txExternalId = externalIdFactory.createFromCommand(command, LoanReAmortizationApiConstants.externalIdParameterName);
@@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.loanaccount.service.reamortization;
import static org.apache.fineract.infrastructure.core.service.DateUtils.getBusinessLocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
@@ -39,7 +38,6 @@ 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;
@@ -123,26 +121,18 @@ public class LoanReAmortizationValidator {
}
}
public void validateUndoReAmortize(Loan loan, JsonCommand command) {
validateUndoReAmortizeBusinessRules(loan);
}
private void validateUndoReAmortizeBusinessRules(Loan loan) {
// validate if there's a reamortization transaction already
Optional<LoanTransaction> optionalReAmortizationTx = loan.getLoanTransactions().stream().filter(tx -> tx.getTypeOf().isReAmortize())
.min(Comparator.comparing(LoanTransaction::getTransactionDate));
public LoanTransaction findAndValidateReAmortizeTransactionForUndo(Loan loan) {
// validate if there's a non-reversed reamortization transaction already
final Optional<LoanTransaction> optionalReAmortizationTx = loan.getLoanTransactions().stream() //
.filter(LoanTransaction::isNotReversed) //
.filter(tx -> tx.getTypeOf().isReAmortize()) //
.findAny();
if (optionalReAmortizationTx.isEmpty()) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reamortize.reamortization.transaction.missing",
"Undoing a reamortization can only be done if there was a reamortization already", loan.getId());
"Undoing a reamortization can only be done if there was a non-reversed reamortization already", loan.getId());
}
// validate if there's no payment between the reamortization and today
boolean repaymentExistsAfterReAmortization = loan.getLoanTransactions().stream().anyMatch(tx -> tx.getTypeOf().isRepaymentType()
&& !tx.isReversed() && transactionHappenedAfterOther(tx, optionalReAmortizationTx.get()));
if (repaymentExistsAfterReAmortization) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reamortize.repayment.exists.after.reamortization",
"Undoing a reamortization can only be done if there hasn't been any repayment afterwards", loan.getId());
}
return optionalReAmortizationTx.get();
}
private void throwExceptionIfValidationErrorsExist(List<ApiParameterError> dataValidationErrors) {
@@ -152,7 +142,4 @@ public class LoanReAmortizationValidator {
}
}
private boolean transactionHappenedAfterOther(LoanTransaction transaction, LoanTransaction otherTransaction) {
return new ChangeOperation(transaction).compareTo(new ChangeOperation(otherTransaction)) > 0;
}
}
@@ -185,53 +185,16 @@ class LoanReAmortizationValidatorTest {
List<LoanTransaction> transactions = List.of(loanTransaction(LoanTransactionType.DISBURSEMENT, actualDate.minusDays(3)));
Loan loan = loan();
given(loan.getLoanTransactions()).willReturn(transactions);
JsonCommand command = jsonCommand();
// when
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
() -> underTest.validateUndoReAmortize(loan, command));
() -> underTest.findAndValidateReAmortizeTransactionForUndo(loan));
// then
assertThat(result).isNotNull();
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reamortize.reamortization.transaction.missing");
}
@Test
public void testValidateUndoReAmortize_ShouldThrowException_WhenLoanAlreadyHasRepaymentAfterReAmortization() {
// given
List<LoanTransaction> transactions = List.of(loanTransaction(LoanTransactionType.DISBURSEMENT, actualDate.minusDays(3)),
loanTransaction(LoanTransactionType.REAMORTIZE, actualDate.minusDays(2)),
loanTransaction(LoanTransactionType.REPAYMENT, actualDate.minusDays(1)));
Loan loan = loan();
given(loan.getLoanTransactions()).willReturn(transactions);
JsonCommand command = jsonCommand();
// when
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
() -> underTest.validateUndoReAmortize(loan, command));
// then
assertThat(result).isNotNull();
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reamortize.repayment.exists.after.reamortization");
}
@Test
public void testValidateUndoReAmortize_ShouldThrowException_WhenLoanAlreadyHasRepaymentAfterReAmortization_SameDay() {
// given
List<LoanTransaction> transactions = List.of(loanTransaction(LoanTransactionType.DISBURSEMENT, actualDate.minusDays(2)),
loanTransaction(LoanTransactionType.REAMORTIZE, actualDate.minusDays(1),
OffsetDateTime.of(actualDate, LocalTime.of(10, 0), ZoneOffset.UTC)),
loanTransaction(LoanTransactionType.REPAYMENT, actualDate.minusDays(1),
OffsetDateTime.of(actualDate, LocalTime.of(11, 0), ZoneOffset.UTC)));
Loan loan = loan();
given(loan.getLoanTransactions()).willReturn(transactions);
JsonCommand command = jsonCommand();
// when
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
() -> underTest.validateUndoReAmortize(loan, command));
// then
assertThat(result).isNotNull();
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reamortize.repayment.exists.after.reamortization");
}
@Test
public void testValidateUndoReAmortize_ShouldNotThrowException_WhenLoanAlreadyHasRepaymentAfterReAmortization_SameDay_RepaymentBeforeReAmortization() {
public void testValidateUndoReAmortize_ShouldNotThrowException() {
// given
List<LoanTransaction> transactions = List.of(loanTransaction(LoanTransactionType.DISBURSEMENT, actualDate.minusDays(2)),
loanTransaction(LoanTransactionType.REAMORTIZE, actualDate.minusDays(1),
@@ -240,9 +203,8 @@ class LoanReAmortizationValidatorTest {
OffsetDateTime.of(actualDate, LocalTime.of(9, 0), ZoneOffset.UTC)));
Loan loan = loan();
given(loan.getLoanTransactions()).willReturn(transactions);
JsonCommand command = jsonCommand();
// when
underTest.validateUndoReAmortize(loan, command);
underTest.findAndValidateReAmortizeTransactionForUndo(loan);
// then no exception thrown
}
@@ -271,6 +233,7 @@ class LoanReAmortizationValidatorTest {
given(loanTransaction.getTypeOf()).willReturn(type);
given(loanTransaction.getTransactionDate()).willReturn(txDate);
given(loanTransaction.getSubmittedOnDate()).willReturn(txDate);
given(loanTransaction.isNotReversed()).willReturn(true);
return loanTransaction;
}