Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.PersistenceContext;
import java.util.function.Supplier;
import org.springframework.stereotype.Component;

@Component
Expand All @@ -38,4 +39,14 @@ public void withFlushMode(FlushModeType flushMode, Runnable runnable) {
entityManager.setFlushMode(original);
}
}

public <T> T withFlushMode(FlushModeType flushMode, Supplier<T> supplier) {
FlushModeType original = entityManager.getFlushMode();
try {
entityManager.setFlushMode(flushMode);
return supplier.get();
} finally {
entityManager.setFlushMode(original);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1811,15 +1811,15 @@ public boolean hasMonetaryActivityAfter(final LocalDate transactionDate) {
}

public boolean hasChargeOffTransaction() {
return getLoanTransactions().stream().anyMatch(LoanTransaction::isChargeOff);
return isChargedOff();
}

public boolean hasAccelerateChargeOffStrategy() {
return LoanChargeOffBehaviour.ACCELERATE_MATURITY.equals(getLoanProductRelatedDetail().getChargeOffBehaviour());
}

public boolean hasContractTerminationTransaction() {
return getLoanTransactions().stream().anyMatch(t -> t.isContractTermination() && t.isNotReversed());
return isContractTermination();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
private Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = new HashSet<>();

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "fromTransaction")
@OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE }, fetch = FetchType.LAZY, mappedBy = "fromTransaction")
private Set<LoanTransactionRelation> loanTransactionRelations = new HashSet<>();

@Setter
Expand Down Expand Up @@ -855,6 +855,10 @@ public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
this.outstandingLoanBalance = outstandingLoanBalance;
}

public BigDecimal getOutstandingLoanBalance() {
return this.outstandingLoanBalance;
}

public boolean isNotRefundForActiveLoan() {
// TODO Auto-generated method stub
return !isRefundForActiveLoan();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ public static LoanTransactionRelation linkToTransaction(@NotNull LoanTransaction
return loanTransactionRelation;
}

public static LoanTransactionRelation createTransactionRelation(@NotNull LoanTransaction fromTransaction,
@NotNull LoanTransaction toTransaction, LoanTransactionRelationTypeEnum relation) {
return new LoanTransactionRelation(fromTransaction, toTransaction, null, relation);
}

public static LoanTransactionRelation linkToCharge(@NotNull LoanTransaction fromTransaction, @NotNull LoanCharge loanCharge,
LoanTransactionRelationTypeEnum relation) {
return new LoanTransactionRelation(fromTransaction, null, loanCharge, relation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,26 @@
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.lang.NonNull;

public interface LoanTransactionRepository extends JpaRepository<LoanTransaction, Long>, JpaSpecificationExecutor<LoanTransaction> {

// Predefined transaction type sets for optimized queries
Set<LoanTransactionType> REPAYMENT_LIKE_TYPES = Set.of(LoanTransactionType.REPAYMENT, LoanTransactionType.RECOVERY_REPAYMENT,
LoanTransactionType.MERCHANT_ISSUED_REFUND, LoanTransactionType.PAYOUT_REFUND, LoanTransactionType.GOODWILL_CREDIT,
LoanTransactionType.CHARGE_REFUND, LoanTransactionType.DOWN_PAYMENT, LoanTransactionType.REFUND,
LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, LoanTransactionType.CREDIT_BALANCE_REFUND, LoanTransactionType.CHARGEBACK,
LoanTransactionType.INTEREST_PAYMENT_WAIVER, LoanTransactionType.INTEREST_REFUND,
LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, LoanTransactionType.CHARGE_ADJUSTMENT,
LoanTransactionType.REPAYMENT_AT_DISBURSEMENT);

Set<LoanTransactionType> EXCLUDED_FROM_COB_TYPES = Set.of(LoanTransactionType.CONTRA, LoanTransactionType.MARKED_FOR_RESCHEDULING,
LoanTransactionType.APPROVE_TRANSFER, LoanTransactionType.INITIATE_TRANSFER, LoanTransactionType.REJECT_TRANSFER,
LoanTransactionType.WITHDRAW_TRANSFER);

Set<LoanTransactionType> EXCLUDED_FROM_RECEIVABLE_INTEREST = Set.of(LoanTransactionType.REPAYMENT_AT_DISBURSEMENT,
LoanTransactionType.DISBURSEMENT);

Optional<LoanTransaction> findByIdAndLoanId(Long transactionId, Long loanId);

@Query("""
Expand Down Expand Up @@ -402,20 +419,14 @@ SELECT MAX(lt.dateOf) FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.amount > 0
AND lt.typeOf IN (
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MERCHANT_ISSUED_REFUND,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.PAYOUT_REFUND,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.GOODWILL_CREDIT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_REFUND,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_ADJUSTMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DOWN_PAYMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INTEREST_PAYMENT_WAIVER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INTEREST_REFUND,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT
)
AND lt.typeOf IN :repaymentLikeTypes
""")
Optional<LocalDate> findLastRepaymentLikeTransactionDate(@Param("loan") Loan loan);
Optional<LocalDate> findLastRepaymentLikeTransactionDate(@Param("loan") Loan loan,
@Param("repaymentLikeTypes") Set<LoanTransactionType> repaymentLikeTypes);

default Optional<LocalDate> findLastRepaymentLikeTransactionDate(Loan loan) {
return findLastRepaymentLikeTransactionDate(loan, REPAYMENT_LIKE_TYPES);
}

@Query("""
SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
Expand Down Expand Up @@ -473,4 +484,142 @@ SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
boolean existsNonReversedByLoanAndTypeAndDate(@Param("loan") Loan loan, @Param("type") LoanTransactionType type,
@Param("transactionDate") LocalDate transactionDate);

@Query("""
SELECT COALESCE(SUM(lt.amount), 0)
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf IN :loanTransactionTypes
""")
BigDecimal sumTotalAmountByLoanAndTransactionTypes(@Param("loan") Loan loan,
@Param("loanTransactionTypes") List<LoanTransactionType> loanTransactionTypes);

// COB Transaction Query for optimization
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.loan IS NOT NULL
AND lt.reversed = false
AND lt.dateOf <= :cobDate
AND lt.typeOf NOT IN :excludedTypes
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
@NonNull
List<LoanTransaction> findTransactionsForCOB(@NonNull @Param("loan") Loan loan, @NonNull @Param("cobDate") LocalDate cobDate,
@Param("excludedTypes") Set<LoanTransactionType> excludedTypes);

@NonNull
default List<LoanTransaction> findTransactionsForCOB(@NonNull Loan loan, @NonNull LocalDate cobDate) {
return findTransactionsForCOB(loan, cobDate, EXCLUDED_FROM_COB_TYPES);
}

// Payment Transactions Query for optimization
@Query("""
SELECT lt
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf IN :repaymentLikeTypes
ORDER BY lt.dateOf ASC, lt.createdDate ASC, lt.id ASC
""")
List<LoanTransaction> findPaymentTransactionsByLoan(@Param("loan") Loan loan,
@Param("repaymentLikeTypes") Set<LoanTransactionType> repaymentLikeTypes);

default List<LoanTransaction> findPaymentTransactionsByLoan(Loan loan) {
return findPaymentTransactionsByLoan(loan, REPAYMENT_LIKE_TYPES);
}

// Disbursement Transactions Query for optimization
@Query("""
SELECT lt
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DISBURSEMENT
ORDER BY lt.dateOf DESC, lt.createdDate DESC, lt.id DESC
""")
List<LoanTransaction> findDisbursementTransactionsByLoanOrderByDateOfDesc(@Param("loan") Loan loan, Pageable pageable);

default Optional<LoanTransaction> findLastDisbursementTransactionByLoan(Loan loan) {
List<LoanTransaction> results = findDisbursementTransactionsByLoanOrderByDateOfDesc(loan, Pageable.ofSize(1));
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}

// Overpayment Calculation Query for optimization
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.loan IS NOT NULL
AND lt.reversed = false
AND lt.typeOf IN :repaymentLikeTypes
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
@NonNull
List<LoanTransaction> findTransactionsForOverpaymentCalculation(@NonNull @Param("loan") Loan loan,
@Param("repaymentLikeTypes") Set<LoanTransactionType> repaymentLikeTypes);

@NonNull
default List<LoanTransaction> findTransactionsForOverpaymentCalculation(@NonNull Loan loan) {
return findTransactionsForOverpaymentCalculation(loan, REPAYMENT_LIKE_TYPES);
}

// Has Disbursement Transaction Query for optimization
@Query("""
SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.loan IS NOT NULL
AND lt.reversed = false
AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DISBURSEMENT
""")
boolean hasDisbursementTransaction(@NonNull @Param("loan") Loan loan);

// Receivable Interest Query for optimization
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.loan IS NOT NULL
AND lt.reversed = false
AND lt.typeOf NOT IN :excludedTypes
AND lt.dateOf <= :tillDate
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
@NonNull
List<LoanTransaction> findTransactionsForReceivableInterest(@NonNull @Param("loan") Loan loan,
@NonNull @Param("tillDate") LocalDate tillDate, @Param("excludedTypes") Set<LoanTransactionType> excludedTypes);

@NonNull
default List<LoanTransaction> findTransactionsForReceivableInterest(@NonNull Loan loan, @NonNull LocalDate tillDate) {
return findTransactionsForReceivableInterest(loan, tillDate, EXCLUDED_FROM_RECEIVABLE_INTEREST);
}

// Outstanding Balance Calculation Query for optimization
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.loan IS NOT NULL
AND lt.reversed = false
AND lt.typeOf NOT IN (
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRA,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MARKED_FOR_RESCHEDULING,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.APPROVE_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ACTIVITY
)
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
@NonNull
List<LoanTransaction> findNonMonetaryTransactionsForOutstandingBalance(@NonNull @Param("loan") Loan loan);

@Query("""
SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.externalId = :externalId
""")
boolean existsByLoanAndExternalId(@Param("loan") Loan loan, @Param("externalId") ExternalId externalId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -457,19 +457,24 @@ private void reprocessChargebackTransactionRelation(ChangedTransactionDetail cha
}
LoanTransactionRelation newLoanTransactionRelation = null;
LoanTransactionRelation oldLoanTransactionRelation = null;
for (LoanTransactionRelation transactionRelation : loanTransaction.getLoanTransactionRelations()) {

Set<LoanTransactionRelation> transactionRelations = loanTransaction.getLoanTransactionRelations();
transactionRelations.size();

for (LoanTransactionRelation transactionRelation : transactionRelations) {
if (LoanTransactionRelationTypeEnum.CHARGEBACK.equals(transactionRelation.getRelationType())
&& oldTransaction != null && oldTransaction.getId() != null
&& oldTransaction.getId().equals(transactionRelation.getToTransaction().getId())) {
newLoanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction,
// Create the relation but don't let it auto-add to avoid collection tracking issues
newLoanTransactionRelation = LoanTransactionRelation.createTransactionRelation(loanTransaction, newTransaction,
LoanTransactionRelationTypeEnum.CHARGEBACK);
oldLoanTransactionRelation = transactionRelation;
break;
}
}
if (newLoanTransactionRelation != null) {
loanTransaction.getLoanTransactionRelations().add(newLoanTransactionRelation);
loanTransaction.getLoanTransactionRelations().remove(oldLoanTransactionRelation);
if (newLoanTransactionRelation != null && oldLoanTransactionRelation != null) {
transactionRelations.remove(oldLoanTransactionRelation);
transactionRelations.add(newLoanTransactionRelation);
}
}
}
Expand Down Expand Up @@ -548,10 +553,15 @@ protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransac
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed");
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
// Adding Replayed relation from newly created transaction to reversed transaction
newLoanTransaction.getLoanTransactionRelations().add(
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
Set<LoanTransactionRelation> originalTransactionRelations = loanTransaction.getLoanTransactionRelations();
originalTransactionRelations.size();
newLoanTransaction.copyLoanTransactionRelations(originalTransactionRelations);

Set<LoanTransactionRelation> newTransactionRelations = newLoanTransaction.getLoanTransactionRelations();
newTransactionRelations.size();
LoanTransactionRelation replayedRelation = LoanTransactionRelation.createTransactionRelation(newLoanTransaction, loanTransaction,
LoanTransactionRelationTypeEnum.REPLAYED);
newTransactionRelations.add(replayedRelation);
changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, newLoanTransaction));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2192,12 +2192,10 @@ public LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final Lo
final LocalDate scheduleTillDate) {
// Loan transactions to process and find the variation on payments
Collection<RecalculationDetail> recalculationDetails = new ArrayList<>();
List<LoanTransaction> transactions = loan.getLoanTransactions();
List<LoanTransaction> transactions = loanTransactionRepository.findPaymentTransactionsByLoan(loan);
for (LoanTransaction loanTransaction : transactions) {
if (loanTransaction.isPaymentTransaction()) {
recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(),
LoanTransaction.copyTransactionProperties(loanTransaction)));
}
recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(),
LoanTransaction.copyTransactionProperties(loanTransaction)));
}
final boolean applyInterestRecalculation = loanApplicationTerms.isInterestBearingAndInterestRecalculationEnabled();

Expand Down
Loading