Skip to content

Commit db9b2d8

Browse files
committed
FINERACT-2324: Phase 1 implementation
1 parent b86efad commit db9b2d8

File tree

22 files changed

+903
-152
lines changed

22 files changed

+903
-152
lines changed

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/persistence/FlushModeHandler.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import jakarta.persistence.EntityManager;
2222
import jakarta.persistence.FlushModeType;
2323
import jakarta.persistence.PersistenceContext;
24+
import java.util.function.Supplier;
2425
import org.springframework.stereotype.Component;
2526

2627
@Component
@@ -38,4 +39,14 @@ public void withFlushMode(FlushModeType flushMode, Runnable runnable) {
3839
entityManager.setFlushMode(original);
3940
}
4041
}
42+
43+
public <T> T withFlushMode(FlushModeType flushMode, Supplier<T> supplier) {
44+
FlushModeType original = entityManager.getFlushMode();
45+
try {
46+
entityManager.setFlushMode(flushMode);
47+
return supplier.get();
48+
} finally {
49+
entityManager.setFlushMode(original);
50+
}
51+
}
4152
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
138138
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY, mappedBy = "loanTransaction")
139139
private Set<LoanTransactionToRepaymentScheduleMapping> loanTransactionToRepaymentScheduleMappings = new HashSet<>();
140140

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

144144
@Setter
@@ -855,6 +855,10 @@ public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
855855
this.outstandingLoanBalance = outstandingLoanBalance;
856856
}
857857

858+
public BigDecimal getOutstandingLoanBalance() {
859+
return this.outstandingLoanBalance;
860+
}
861+
858862
public boolean isNotRefundForActiveLoan() {
859863
// TODO Auto-generated method stub
860864
return !isRefundForActiveLoan();

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ public static LoanTransactionRelation linkToTransaction(@NotNull LoanTransaction
6969
return loanTransactionRelation;
7070
}
7171

72+
public static LoanTransactionRelation createTransactionRelation(@NotNull LoanTransaction fromTransaction,
73+
@NotNull LoanTransaction toTransaction, LoanTransactionRelationTypeEnum relation) {
74+
return new LoanTransactionRelation(fromTransaction, toTransaction, null, relation);
75+
}
76+
7277
public static LoanTransactionRelation linkToCharge(@NotNull LoanTransaction fromTransaction, @NotNull LoanCharge loanCharge,
7378
LoanTransactionRelationTypeEnum relation) {
7479
return new LoanTransactionRelation(fromTransaction, null, loanCharge, relation);

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java

Lines changed: 152 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,26 @@
3434
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
3535
import org.springframework.data.jpa.repository.Query;
3636
import org.springframework.data.repository.query.Param;
37+
import org.springframework.lang.NonNull;
3738

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

41+
// Predefined transaction type sets for optimized queries
42+
Set<LoanTransactionType> REPAYMENT_LIKE_TYPES = Set.of(LoanTransactionType.REPAYMENT, LoanTransactionType.RECOVERY_REPAYMENT,
43+
LoanTransactionType.MERCHANT_ISSUED_REFUND, LoanTransactionType.PAYOUT_REFUND, LoanTransactionType.GOODWILL_CREDIT,
44+
LoanTransactionType.CHARGE_REFUND, LoanTransactionType.DOWN_PAYMENT, LoanTransactionType.REFUND,
45+
LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, LoanTransactionType.CREDIT_BALANCE_REFUND, LoanTransactionType.CHARGEBACK,
46+
LoanTransactionType.INTEREST_PAYMENT_WAIVER, LoanTransactionType.INTEREST_REFUND,
47+
LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT, LoanTransactionType.CHARGE_ADJUSTMENT,
48+
LoanTransactionType.REPAYMENT_AT_DISBURSEMENT);
49+
50+
Set<LoanTransactionType> EXCLUDED_FROM_COB_TYPES = Set.of(LoanTransactionType.CONTRA, LoanTransactionType.MARKED_FOR_RESCHEDULING,
51+
LoanTransactionType.APPROVE_TRANSFER, LoanTransactionType.INITIATE_TRANSFER, LoanTransactionType.REJECT_TRANSFER,
52+
LoanTransactionType.WITHDRAW_TRANSFER);
53+
54+
Set<LoanTransactionType> EXCLUDED_FROM_RECEIVABLE_INTEREST = Set.of(LoanTransactionType.REPAYMENT_AT_DISBURSEMENT,
55+
LoanTransactionType.DISBURSEMENT);
56+
4057
Optional<LoanTransaction> findByIdAndLoanId(Long transactionId, Long loanId);
4158

4259
@Query("""
@@ -402,20 +419,14 @@ SELECT MAX(lt.dateOf) FROM LoanTransaction lt
402419
WHERE lt.loan = :loan
403420
AND lt.reversed = false
404421
AND lt.amount > 0
405-
AND lt.typeOf IN (
406-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT,
407-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MERCHANT_ISSUED_REFUND,
408-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.PAYOUT_REFUND,
409-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.GOODWILL_CREDIT,
410-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_REFUND,
411-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_ADJUSTMENT,
412-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DOWN_PAYMENT,
413-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INTEREST_PAYMENT_WAIVER,
414-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INTEREST_REFUND,
415-
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT
416-
)
422+
AND lt.typeOf IN :repaymentLikeTypes
417423
""")
418-
Optional<LocalDate> findLastRepaymentLikeTransactionDate(@Param("loan") Loan loan);
424+
Optional<LocalDate> findLastRepaymentLikeTransactionDate(@Param("loan") Loan loan,
425+
@Param("repaymentLikeTypes") Set<LoanTransactionType> repaymentLikeTypes);
426+
427+
default Optional<LocalDate> findLastRepaymentLikeTransactionDate(Loan loan) {
428+
return findLastRepaymentLikeTransactionDate(loan, REPAYMENT_LIKE_TYPES);
429+
}
419430

420431
@Query("""
421432
SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
@@ -483,4 +494,132 @@ SELECT COALESCE(SUM(lt.amount), 0)
483494
BigDecimal sumTotalAmountByLoanAndTransactionTypes(@Param("loan") Loan loan,
484495
@Param("loanTransactionTypes") List<LoanTransactionType> loanTransactionTypes);
485496

497+
// COB Transaction Query for optimization
498+
@Query("""
499+
SELECT lt FROM LoanTransaction lt
500+
WHERE lt.loan = :loan
501+
AND lt.loan IS NOT NULL
502+
AND lt.reversed = false
503+
AND lt.dateOf <= :cobDate
504+
AND lt.typeOf NOT IN :excludedTypes
505+
ORDER BY lt.dateOf, lt.createdDate, lt.id
506+
""")
507+
@NonNull
508+
List<LoanTransaction> findTransactionsForCOB(@NonNull @Param("loan") Loan loan, @NonNull @Param("cobDate") LocalDate cobDate,
509+
@Param("excludedTypes") Set<LoanTransactionType> excludedTypes);
510+
511+
@NonNull
512+
default List<LoanTransaction> findTransactionsForCOB(@NonNull Loan loan, @NonNull LocalDate cobDate) {
513+
return findTransactionsForCOB(loan, cobDate, EXCLUDED_FROM_COB_TYPES);
514+
}
515+
516+
// Payment Transactions Query for optimization
517+
@Query("""
518+
SELECT lt
519+
FROM LoanTransaction lt
520+
WHERE lt.loan = :loan
521+
AND lt.reversed = false
522+
AND lt.typeOf IN :repaymentLikeTypes
523+
ORDER BY lt.dateOf ASC, lt.createdDate ASC, lt.id ASC
524+
""")
525+
List<LoanTransaction> findPaymentTransactionsByLoan(@Param("loan") Loan loan,
526+
@Param("repaymentLikeTypes") Set<LoanTransactionType> repaymentLikeTypes);
527+
528+
default List<LoanTransaction> findPaymentTransactionsByLoan(Loan loan) {
529+
return findPaymentTransactionsByLoan(loan, REPAYMENT_LIKE_TYPES);
530+
}
531+
532+
// Disbursement Transactions Query for optimization
533+
@Query("""
534+
SELECT lt
535+
FROM LoanTransaction lt
536+
WHERE lt.loan = :loan
537+
AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DISBURSEMENT
538+
ORDER BY lt.dateOf DESC, lt.createdDate DESC, lt.id DESC
539+
""")
540+
List<LoanTransaction> findDisbursementTransactionsByLoanOrderByDateOfDesc(@Param("loan") Loan loan, Pageable pageable);
541+
542+
default Optional<LoanTransaction> findLastDisbursementTransactionByLoan(Loan loan) {
543+
List<LoanTransaction> results = findDisbursementTransactionsByLoanOrderByDateOfDesc(loan, Pageable.ofSize(1));
544+
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
545+
}
546+
547+
// Overpayment Calculation Query for optimization
548+
@Query("""
549+
SELECT lt FROM LoanTransaction lt
550+
WHERE lt.loan = :loan
551+
AND lt.loan IS NOT NULL
552+
AND lt.reversed = false
553+
AND lt.typeOf IN :repaymentLikeTypes
554+
ORDER BY lt.dateOf, lt.createdDate, lt.id
555+
""")
556+
@NonNull
557+
List<LoanTransaction> findTransactionsForOverpaymentCalculation(@NonNull @Param("loan") Loan loan,
558+
@Param("repaymentLikeTypes") Set<LoanTransactionType> repaymentLikeTypes);
559+
560+
@NonNull
561+
default List<LoanTransaction> findTransactionsForOverpaymentCalculation(@NonNull Loan loan) {
562+
return findTransactionsForOverpaymentCalculation(loan, REPAYMENT_LIKE_TYPES);
563+
}
564+
565+
// Has Disbursement Transaction Query for optimization
566+
@Query("""
567+
SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
568+
FROM LoanTransaction lt
569+
WHERE lt.loan = :loan
570+
AND lt.loan IS NOT NULL
571+
AND lt.reversed = false
572+
AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DISBURSEMENT
573+
""")
574+
boolean hasDisbursementTransaction(@NonNull @Param("loan") Loan loan);
575+
576+
// Receivable Interest Query for optimization
577+
@Query("""
578+
SELECT lt FROM LoanTransaction lt
579+
WHERE lt.loan = :loan
580+
AND lt.loan IS NOT NULL
581+
AND lt.reversed = false
582+
AND lt.typeOf NOT IN :excludedTypes
583+
AND lt.dateOf <= :tillDate
584+
ORDER BY lt.dateOf, lt.createdDate, lt.id
585+
""")
586+
@NonNull
587+
List<LoanTransaction> findTransactionsForReceivableInterest(@NonNull @Param("loan") Loan loan,
588+
@NonNull @Param("tillDate") LocalDate tillDate, @Param("excludedTypes") Set<LoanTransactionType> excludedTypes);
589+
590+
@NonNull
591+
default List<LoanTransaction> findTransactionsForReceivableInterest(@NonNull Loan loan, @NonNull LocalDate tillDate) {
592+
return findTransactionsForReceivableInterest(loan, tillDate, EXCLUDED_FROM_RECEIVABLE_INTEREST);
593+
}
594+
595+
// Outstanding Balance Calculation Query for optimization
596+
@Query("""
597+
SELECT lt FROM LoanTransaction lt
598+
WHERE lt.loan = :loan
599+
AND lt.loan IS NOT NULL
600+
AND lt.reversed = false
601+
AND lt.typeOf NOT IN (
602+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRA,
603+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MARKED_FOR_RESCHEDULING,
604+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.APPROVE_TRANSFER,
605+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER,
606+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER,
607+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER,
608+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL,
609+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT,
610+
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ACTIVITY
611+
)
612+
ORDER BY lt.dateOf, lt.createdDate, lt.id
613+
""")
614+
@NonNull
615+
List<LoanTransaction> findNonMonetaryTransactionsForOutstandingBalance(@NonNull @Param("loan") Loan loan);
616+
617+
@Query("""
618+
SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
619+
FROM LoanTransaction lt
620+
WHERE lt.loan = :loan
621+
AND lt.externalId = :externalId
622+
""")
623+
boolean existsByLoanAndExternalId(@Param("loan") Loan loan, @Param("externalId") ExternalId externalId);
624+
486625
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -457,19 +457,24 @@ private void reprocessChargebackTransactionRelation(ChangedTransactionDetail cha
457457
}
458458
LoanTransactionRelation newLoanTransactionRelation = null;
459459
LoanTransactionRelation oldLoanTransactionRelation = null;
460-
for (LoanTransactionRelation transactionRelation : loanTransaction.getLoanTransactionRelations()) {
460+
461+
Set<LoanTransactionRelation> transactionRelations = loanTransaction.getLoanTransactionRelations();
462+
transactionRelations.size();
463+
464+
for (LoanTransactionRelation transactionRelation : transactionRelations) {
461465
if (LoanTransactionRelationTypeEnum.CHARGEBACK.equals(transactionRelation.getRelationType())
462466
&& oldTransaction != null && oldTransaction.getId() != null
463467
&& oldTransaction.getId().equals(transactionRelation.getToTransaction().getId())) {
464-
newLoanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction,
468+
// Create the relation but don't let it auto-add to avoid collection tracking issues
469+
newLoanTransactionRelation = LoanTransactionRelation.createTransactionRelation(loanTransaction, newTransaction,
465470
LoanTransactionRelationTypeEnum.CHARGEBACK);
466471
oldLoanTransactionRelation = transactionRelation;
467472
break;
468473
}
469474
}
470-
if (newLoanTransactionRelation != null) {
471-
loanTransaction.getLoanTransactionRelations().add(newLoanTransactionRelation);
472-
loanTransaction.getLoanTransactionRelations().remove(oldLoanTransactionRelation);
475+
if (newLoanTransactionRelation != null && oldLoanTransactionRelation != null) {
476+
transactionRelations.remove(oldLoanTransactionRelation);
477+
transactionRelations.add(newLoanTransactionRelation);
473478
}
474479
}
475480
}
@@ -548,10 +553,15 @@ protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransac
548553
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed");
549554
loanTransaction.reverse();
550555
loanTransaction.updateExternalId(null);
551-
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
552-
// Adding Replayed relation from newly created transaction to reversed transaction
553-
newLoanTransaction.getLoanTransactionRelations().add(
554-
LoanTransactionRelation.linkToTransaction(newLoanTransaction, loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
556+
Set<LoanTransactionRelation> originalTransactionRelations = loanTransaction.getLoanTransactionRelations();
557+
originalTransactionRelations.size();
558+
newLoanTransaction.copyLoanTransactionRelations(originalTransactionRelations);
559+
560+
Set<LoanTransactionRelation> newTransactionRelations = newLoanTransaction.getLoanTransactionRelations();
561+
newTransactionRelations.size();
562+
LoanTransactionRelation replayedRelation = LoanTransactionRelation.createTransactionRelation(newLoanTransaction, loanTransaction,
563+
LoanTransactionRelationTypeEnum.REPLAYED);
564+
newTransactionRelations.add(replayedRelation);
555565
changedTransactionDetail.addTransactionChange(new TransactionChangeData(loanTransaction, newLoanTransaction));
556566
}
557567

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractCumulativeLoanScheduleGenerator.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2192,12 +2192,10 @@ public LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final Lo
21922192
final LocalDate scheduleTillDate) {
21932193
// Loan transactions to process and find the variation on payments
21942194
Collection<RecalculationDetail> recalculationDetails = new ArrayList<>();
2195-
List<LoanTransaction> transactions = loan.getLoanTransactions();
2195+
List<LoanTransaction> transactions = loanTransactionRepository.findPaymentTransactionsByLoan(loan);
21962196
for (LoanTransaction loanTransaction : transactions) {
2197-
if (loanTransaction.isPaymentTransaction()) {
2198-
recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(),
2199-
LoanTransaction.copyTransactionProperties(loanTransaction)));
2200-
}
2197+
recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(),
2198+
LoanTransaction.copyTransactionProperties(loanTransaction)));
22012199
}
22022200
final boolean applyInterestRecalculation = loanApplicationTerms.isInterestBearingAndInterestRecalculationEnabled();
22032201

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
*/
1919
package org.apache.fineract.portfolio.loanaccount.service;
2020

21-
import jakarta.persistence.FlushModeType;
2221
import java.math.BigDecimal;
2322
import java.time.LocalDate;
2423
import java.util.ArrayList;
@@ -93,16 +92,14 @@ public boolean isOverPaid(final Loan loan) {
9392
}
9493

9594
public void updateLoanSummaryDerivedFields(final Loan loan) {
96-
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
97-
if (loan.isNotDisbursed()) {
98-
if (loan.getSummary() != null) {
99-
loan.getSummary().zeroFields();
100-
}
101-
loan.setTotalOverpaid(null);
102-
} else {
103-
refreshSummaryAndBalancesForDisbursedLoan(loan);
95+
if (loan.isNotDisbursed()) {
96+
if (loan.getSummary() != null) {
97+
loan.getSummary().zeroFields();
10498
}
105-
});
99+
loan.setTotalOverpaid(null);
100+
} else {
101+
refreshSummaryAndBalancesForDisbursedLoan(loan);
102+
}
106103
}
107104

108105
public void refreshSummaryAndBalancesForDisbursedLoan(final Loan loan) {

0 commit comments

Comments
 (0)