FINERACT-2324: Remove getLoanTransactions from accounting

This commit is contained in:
Oleksii Novikov
2025-08-13 14:39:01 +02:00
committed by Adam Saghy
parent 87c0a02402
commit ebb6b6c500
2 changed files with 130 additions and 66 deletions
@@ -481,4 +481,55 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
""") """)
BigDecimal calculateTotalRecoveryPaymentAmount(@Param("loan") Loan loan); BigDecimal calculateTotalRecoveryPaymentAmount(@Param("loan") Loan loan);
@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 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
""")
List<LoanTransaction> findTransactionsForChargeOffClassification(@Param("loan") Loan loan,
@Param("chargeOffDate") LocalDate chargeOffDate, @Param("dateComparison") String dateComparison);
} }
@@ -19,13 +19,12 @@
package org.apache.fineract.portfolio.loanaccount.mapper; package org.apache.fineract.portfolio.loanaccount.mapper;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO; import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO; import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByDTO;
@@ -36,6 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -75,15 +75,8 @@ public class LoanAccountingBridgeMapper {
public AccountingBridgeDataDTO deriveAccountingBridgeData(final String currencyCode, final List<Long> existingTransactionIds, public AccountingBridgeDataDTO deriveAccountingBridgeData(final String currencyCode, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final boolean isAccountTransfer, final Loan loan) { final List<Long> existingReversedTransactionIds, final boolean isAccountTransfer, final Loan loan) {
List<LoanTransaction> transactions; final List<LoanTransaction> transactions = findTransactionsForAccountingBridge(existingTransactionIds,
if (existingTransactionIds == null || existingTransactionIds.isEmpty()) { existingReversedTransactionIds, loan);
transactions = loanTransactionRepository.findNonReversedByLoan(loan);
} else if (existingReversedTransactionIds == null || existingReversedTransactionIds.isEmpty()) {
transactions = loanTransactionRepository.findTransactionsForAccountingBridge(loan, existingTransactionIds);
} else {
transactions = loanTransactionRepository.findTransactionsForAccountingBridge(loan, existingTransactionIds,
existingReversedTransactionIds);
}
final List<AccountingBridgeLoanTransactionDTO> newLoanTransactions = transactions.stream() // final List<AccountingBridgeLoanTransactionDTO> newLoanTransactions = transactions.stream() //
.map(transaction -> mapToLoanTransactionData(transaction, currencyCode)) // .map(transaction -> mapToLoanTransactionData(transaction, currencyCode)) //
@@ -179,47 +172,53 @@ public class LoanAccountingBridgeMapper {
private void classifyTransactionsBasedOnChargeOffDate(final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsBeforeChargeOff, private void classifyTransactionsBasedOnChargeOffDate(final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsBeforeChargeOff,
final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsAfterChargeOff, final List<Long> existingTransactionIds, final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsAfterChargeOff, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final String currencyCode, final Loan loan) { final List<Long> existingReversedTransactionIds, final String currencyCode, final Loan loan) {
// Before
filterTransactionsByChargeOffDate(newLoanTransactionsBeforeChargeOff, currencyCode, existingTransactionIds, final LocalDate chargeOffDate = loan.getChargedOffOnDate();
existingReversedTransactionIds, if (chargeOffDate == null) {
transaction -> DateUtils.isBefore(transaction.getTransactionDate(), loan.getChargedOffOnDate()), loan); return;
// On }
filterTransactionsByChargeOffDate(newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff, currencyCode,
existingTransactionIds, existingReversedTransactionIds, // Before charge-off
transaction -> DateUtils.isEqual(transaction.getTransactionDate(), loan.getChargedOffOnDate()), loan); final List<LoanTransaction> beforeTransactions = findTransactionsForChargeOffClassification(loan, chargeOffDate, "BEFORE",
// After existingTransactionIds, existingReversedTransactionIds);
filterTransactionsByChargeOffDate(newLoanTransactionsAfterChargeOff, currencyCode, existingTransactionIds, processTransactionsForChargeOff(beforeTransactions, newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff,
existingReversedTransactionIds, currencyCode, loan);
transaction -> DateUtils.isAfter(transaction.getTransactionDate(), loan.getChargedOffOnDate()), loan);
// On charge-off date
final List<LoanTransaction> onTransactions = findTransactionsForChargeOffClassification(loan, chargeOffDate, "EQUAL",
existingTransactionIds, existingReversedTransactionIds);
processTransactionsForChargeOff(onTransactions, newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff, currencyCode,
loan);
// After charge-off
final List<LoanTransaction> afterTransactions = findTransactionsForChargeOffClassification(loan, chargeOffDate, "AFTER",
existingTransactionIds, existingReversedTransactionIds);
processTransactionsForChargeOff(afterTransactions, newLoanTransactionsBeforeChargeOff, newLoanTransactionsAfterChargeOff,
currencyCode, loan);
} }
private void filterTransactionsByChargeOffDate(final List<AccountingBridgeLoanTransactionDTO> filteredTransactions, private List<LoanTransaction> findTransactionsForChargeOffClassification(final Loan loan, final LocalDate chargeOffDate,
final String currencyCode, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final String dateComparison, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
final Predicate<LoanTransaction> chargeOffDateCriteria, final Loan loan) {
filteredTransactions.addAll(loan.getLoanTransactions().stream() //
.filter(chargeOffDateCriteria) //
.filter(transaction -> {
boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId());
boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId());
if (transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction) { final boolean hasExistingIds = existingTransactionIds != null && !existingTransactionIds.isEmpty();
return true; final boolean hasExistingReversedIds = existingReversedTransactionIds != null && !existingReversedTransactionIds.isEmpty();
} else {
return !isExistingTransaction; if (hasExistingIds && hasExistingReversedIds) {
} return loanTransactionRepository.findTransactionsForChargeOffClassification(loan, chargeOffDate, dateComparison,
}) // existingTransactionIds, existingReversedTransactionIds);
.map(transaction -> mapToLoanTransactionData(transaction, currencyCode)).toList()); } else if (hasExistingIds) {
return loanTransactionRepository.findTransactionsForChargeOffClassification(loan, chargeOffDate, dateComparison,
existingTransactionIds);
} else {
return loanTransactionRepository.findTransactionsForChargeOffClassification(loan, chargeOffDate, dateComparison);
}
} }
private void filterTransactionsByChargeOffDate(final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsBeforeChargeOff, private void processTransactionsForChargeOff(final List<LoanTransaction> transactions,
final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsAfterChargeOff, final String currencyCode, final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsBeforeChargeOff,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final List<AccountingBridgeLoanTransactionDTO> newLoanTransactionsAfterChargeOff, final String currencyCode, final Loan loan) {
final Predicate<LoanTransaction> chargeOffDateCriteria, final Loan loan) {
final Optional<LoanTransaction> chargeOffTransactionOptional = loan.getLoanTransactions().stream() //
.filter(LoanTransaction::isChargeOff) //
.filter(LoanTransaction::isNotReversed) //
.findFirst();
final Optional<LoanTransaction> chargeOffTransactionOptional = findChargeOffTransaction(loan);
if (chargeOffTransactionOptional.isEmpty()) { if (chargeOffTransactionOptional.isEmpty()) {
return; return;
} }
@@ -227,35 +226,49 @@ public class LoanAccountingBridgeMapper {
final LoanTransaction chargeOffTransaction = chargeOffTransactionOptional.get(); final LoanTransaction chargeOffTransaction = chargeOffTransactionOptional.get();
final LoanTransaction originalChargeOffTransaction = getOriginalTransactionIfReverseReplayed(chargeOffTransaction); final LoanTransaction originalChargeOffTransaction = getOriginalTransactionIfReverseReplayed(chargeOffTransaction);
loan.getLoanTransactions().stream().filter(chargeOffDateCriteria).forEach(transaction -> { transactions.forEach(transaction -> {
boolean isExistingTransaction = existingTransactionIds.contains(transaction.getId()); List<AccountingBridgeLoanTransactionDTO> targetList;
boolean isExistingReversedTransaction = existingReversedTransactionIds.contains(transaction.getId());
List<AccountingBridgeLoanTransactionDTO> targetList = null; if (transaction.isReversed()) {
if ((transaction.isReversed() && isExistingTransaction && !isExistingReversedTransaction)) { final LoanTransaction originalTransaction = getOriginalTransactionIfReverseReplayed(transaction);
// reversed transactions
LoanTransaction originalTransaction = getOriginalTransactionIfReverseReplayed(transaction);
targetList = originalTransaction.happenedBefore(originalChargeOffTransaction) ? newLoanTransactionsBeforeChargeOff targetList = originalTransaction.happenedBefore(originalChargeOffTransaction) ? newLoanTransactionsBeforeChargeOff
: newLoanTransactionsAfterChargeOff; : newLoanTransactionsAfterChargeOff;
} else {
} else if (!isExistingTransaction) {
// new and replayed transactions
targetList = transaction.happenedBefore(chargeOffTransaction) ? newLoanTransactionsBeforeChargeOff targetList = transaction.happenedBefore(chargeOffTransaction) ? newLoanTransactionsBeforeChargeOff
: newLoanTransactionsAfterChargeOff; : newLoanTransactionsAfterChargeOff;
} }
if (targetList != null) {
targetList.add(mapToLoanTransactionData(transaction, currencyCode)); targetList.add(mapToLoanTransactionData(transaction, currencyCode));
}
}); });
} }
private LoanTransaction getOriginalTransactionIfReverseReplayed(final LoanTransaction loanTransaction) { private Optional<LoanTransaction> findChargeOffTransaction(final Loan loan) {
if (!loanTransaction.getLoanTransactionRelations().isEmpty()) { return loanTransactionRepository.findNonReversedByLoanAndType(loan, LoanTransactionType.CHARGE_OFF).stream() //
return loanTransaction.getLoanTransactionRelations().stream() .findFirst();
.filter(tr -> LoanTransactionRelationTypeEnum.REPLAYED.equals(tr.getRelationType())) }
.map(LoanTransactionRelation::getToTransaction).toList().stream().min(Comparator.comparingLong(LoanTransaction::getId))
.orElse(loanTransaction); private List<LoanTransaction> findTransactionsForAccountingBridge(final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final Loan loan) {
if (existingTransactionIds == null || existingTransactionIds.isEmpty()) {
return loanTransactionRepository.findNonReversedByLoan(loan);
} else if (existingReversedTransactionIds == null || existingReversedTransactionIds.isEmpty()) {
return loanTransactionRepository.findTransactionsForAccountingBridge(loan, existingTransactionIds);
} else {
return loanTransactionRepository.findTransactionsForAccountingBridge(loan, existingTransactionIds,
existingReversedTransactionIds);
} }
return loanTransaction; }
private LoanTransaction getOriginalTransactionIfReverseReplayed(final LoanTransaction loanTransaction) {
if (loanTransaction.getLoanTransactionRelations().isEmpty()) {
return loanTransaction;
}
return loanTransaction.getLoanTransactionRelations().stream() //
.filter(tr -> LoanTransactionRelationTypeEnum.REPLAYED.equals(tr.getRelationType())) //
.map(LoanTransactionRelation::getToTransaction) //
.min(Comparator.comparingLong(LoanTransaction::getId)) //
.orElse(loanTransaction);
} }
} }