FINERACT-2354: Allow re-age today if the previous re-age was reversed

This commit is contained in:
Oleksii Novikov
2025-09-09 12:12:22 +02:00
committed by Adam Saghy
parent 5b1d72d15b
commit 7ee1919ddb
3 changed files with 34 additions and 85 deletions
@@ -314,36 +314,6 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
Optional<LocalDate> findLastNonReversedTransactionDateByLoanAndTypes(@Param("loan") Loan loan,
@Param("types") Set<LoanTransactionType> types);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND (
(lt.reversed = true AND lt.id IN :existingTransactionIds AND lt.id NOT IN :existingReversedTransactionIds)
OR (lt.id NOT IN :existingTransactionIds)
)
""")
List<LoanTransaction> findTransactionsForAccountingBridge(@Param("loan") Loan loan,
@Param("existingTransactionIds") List<Long> existingTransactionIds,
@Param("existingReversedTransactionIds") List<Long> existingReversedTransactionIds);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND (
(lt.reversed = true AND lt.id IN :existingTransactionIds)
OR (lt.id NOT IN :existingTransactionIds)
)
""")
List<LoanTransaction> findTransactionsForAccountingBridge(@Param("loan") Loan loan,
@Param("existingTransactionIds") List<Long> existingTransactionIds);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
""")
List<LoanTransaction> findNonReversedByLoan(@Param("loan") Loan loan);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
@@ -482,54 +452,14 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
BigDecimal calculateTotalRecoveryPaymentAmount(@Param("loan") Loan loan);
@Query("""
SELECT lt FROM LoanTransaction lt
SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND (
(:dateComparison = 'BEFORE' AND lt.dateOf < :chargeOffDate) OR
(:dateComparison = 'EQUAL' AND lt.dateOf = :chargeOffDate) OR
(:dateComparison = 'AFTER' AND lt.dateOf > :chargeOffDate)
)
AND (
(lt.reversed = true AND lt.id IN :existingTransactionIds AND lt.id NOT IN :existingReversedTransactionIds)
OR (lt.id NOT IN :existingTransactionIds)
)
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findTransactionsForChargeOffClassification(@Param("loan") Loan loan,
@Param("chargeOffDate") LocalDate chargeOffDate, @Param("dateComparison") String dateComparison,
@Param("existingTransactionIds") List<Long> existingTransactionIds,
@Param("existingReversedTransactionIds") List<Long> existingReversedTransactionIds);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND (
(:dateComparison = 'BEFORE' AND lt.dateOf < :chargeOffDate) OR
(:dateComparison = 'EQUAL' AND lt.dateOf = :chargeOffDate) OR
(:dateComparison = 'AFTER' AND lt.dateOf > :chargeOffDate)
)
AND (
(lt.reversed = true AND lt.id IN :existingTransactionIds)
OR (lt.id NOT IN :existingTransactionIds)
)
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findTransactionsForChargeOffClassification(@Param("loan") Loan loan,
@Param("chargeOffDate") LocalDate chargeOffDate, @Param("dateComparison") String dateComparison,
@Param("existingTransactionIds") List<Long> existingTransactionIds);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND (
(:dateComparison = 'BEFORE' AND lt.dateOf < :chargeOffDate) OR
(:dateComparison = 'EQUAL' AND lt.dateOf = :chargeOffDate) OR
(:dateComparison = 'AFTER' AND lt.dateOf > :chargeOffDate)
)
AND lt.reversed = false
ORDER BY lt.dateOf, lt.createdDate, lt.id
AND lt.typeOf = :type
AND lt.dateOf = :transactionDate
""")
List<LoanTransaction> findTransactionsForChargeOffClassification(@Param("loan") Loan loan,
@Param("chargeOffDate") LocalDate chargeOffDate, @Param("dateComparison") String dateComparison);
boolean existsNonReversedByLoanAndTypeAndDate(@Param("loan") Loan loan, @Param("type") LoanTransactionType type,
@Param("transactionDate") LocalDate transactionDate);
}
@@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
@@ -34,14 +35,19 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
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.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 LoanReAgingValidator {
private final LoanTransactionRepository loanTransactionRepository;
public void validateReAge(Loan loan, JsonCommand command) {
validateReAgeRequest(loan, command);
validateReAgeBusinessRules(loan);
@@ -107,8 +113,9 @@ public class LoanReAgingValidator {
}
// validate if there's already a re-aging transaction for today
boolean isReAgingTransactionForTodayPresent = loan.getLoanTransactions().stream()
.anyMatch(tx -> tx.getTypeOf().isReAge() && tx.getTransactionDate().equals(getBusinessLocalDate()));
final boolean isReAgingTransactionForTodayPresent = loanTransactionRepository.existsNonReversedByLoanAndTypeAndDate(loan,
LoanTransactionType.REAGE, getBusinessLocalDate());
if (isReAgingTransactionForTodayPresent) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.reage.transaction.already.present.for.today",
"Loan reaging can only be done once a day. There has already been a reaging done for today", loan.getId());
@@ -34,7 +34,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
@@ -46,6 +45,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
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.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor;
@@ -54,18 +54,30 @@ 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.Mock;
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 LoanReAgingValidatorTest {
@Mock
private LoanTransactionRepository loanTransactionRepository;
@InjectMocks
private LoanReAgingValidator underTest;
public static final String DATE_FORMAT = "dd MMMM yyyy";
private final LocalDate actualDate = LocalDate.now(Clock.systemUTC());
private final LocalDate maturityDate = actualDate.plusDays(30);
private final LocalDate businessDate = maturityDate.plusDays(1);
private final LocalDate afterMaturity = maturityDate.plusDays(7);
private LoanReAgingValidator underTest = new LoanReAgingValidator();
@BeforeEach
public void setUp() {
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
@@ -92,7 +104,8 @@ class LoanReAgingValidatorTest {
public void testValidateReAge_ShouldThrowException_WhenExternalIdIsLongerThan100() {
// given
Loan loan = loan();
JsonCommand command = jsonCommand(RandomStringUtils.randomAlphabetic(120));
String longExternalId = "A".repeat(120);
JsonCommand command = jsonCommand(longExternalId);
// when
PlatformApiDataValidationException result = assertThrows(PlatformApiDataValidationException.class,
() -> underTest.validateReAge(loan, command));
@@ -367,10 +380,9 @@ class LoanReAgingValidatorTest {
@Test
public void testValidateReAge_ShouldThrowException_WhenLoanAlreadyHasReAgeForToday() {
// given
List<LoanTransaction> transactions = List.of(loanTransaction(LoanTransactionType.DISBURSEMENT, maturityDate.minusDays(2)),
loanTransaction(LoanTransactionType.REAGE, businessDate));
Loan loan = loan();
given(loan.getLoanTransactions()).willReturn(transactions);
given(loanTransactionRepository.existsNonReversedByLoanAndTypeAndDate(loan, LoanTransactionType.REAGE, businessDate))
.willReturn(true);
JsonCommand command = jsonCommand();
// when
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,