From f6f8c6f4a55864faa9718b46c0d8d42e70ce9874 Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Tue, 20 May 2025 12:39:06 +0300 Subject: [PATCH] FINERACT-2274: Rework Accrual service --- .../CumulativeIncomeFromIncomePosting.java | 24 ++ .../TransactionPortionsForForeclosure.java | 34 +++ .../domain/LoanChargePaidByRepository.java | 18 +- .../domain/LoanTransactionRepository.java | 222 ++++++++++++-- .../mapper/LoanAccountingBridgeMapper.java | 23 +- ...ccrualTransactionBusinessEventService.java | 1 + ...alTransactionBusinessEventServiceImpl.java | 18 +- .../LoanAccrualsProcessingService.java | 6 +- .../service/LoanTransactionReadService.java | 2 - .../AddAccrualEntriesTasklet.java | 9 +- .../LoanAccrualsProcessingServiceImpl.java | 287 +++++++----------- 11 files changed, 419 insertions(+), 225 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CumulativeIncomeFromIncomePosting.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/TransactionPortionsForForeclosure.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CumulativeIncomeFromIncomePosting.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CumulativeIncomeFromIncomePosting.java new file mode 100644 index 000000000..b55161a97 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CumulativeIncomeFromIncomePosting.java @@ -0,0 +1,24 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.data; + +import java.math.BigDecimal; + +public record CumulativeIncomeFromIncomePosting(BigDecimal interestAmount, BigDecimal feeAmount, BigDecimal penaltyAmount) { +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/TransactionPortionsForForeclosure.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/TransactionPortionsForForeclosure.java new file mode 100644 index 000000000..64d31f8b3 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/TransactionPortionsForForeclosure.java @@ -0,0 +1,34 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.data; + +import java.math.BigDecimal; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; + +public interface TransactionPortionsForForeclosure { + + LoanTransactionType getTransactionType(); + + BigDecimal getInterestPortion(); + + BigDecimal getFeeChargesPortion(); + + BigDecimal getPenaltyChargesPortion(); + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidByRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidByRepository.java index dc8d36a4f..be73c209a 100755 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidByRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanChargePaidByRepository.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; @@ -25,7 +26,18 @@ import org.springframework.data.repository.query.Param; public interface LoanChargePaidByRepository extends JpaRepository, JpaSpecificationExecutor { - @Query("select lp from LoanChargePaidBy lp where lp.loanCharge=:loanCharge and lp.installmentNumber=:installmentNumber") - LoanChargePaidBy getLoanChargePaidByLoanCharge(@Param("loanCharge") LoanCharge loanCharge, - @Param("installmentNumber") Integer installmentNo); + @Query(""" + SELECT lcpb + FROM LoanChargePaidBy lcpb + JOIN lcpb.loanTransaction lt + JOIN lcpb.loanCharge lc + WHERE lt.loan = :loan + AND lt.reversed = false + AND (lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL + OR lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT) + AND (lt.feeChargesPortion > 0 OR lt.penaltyChargesPortion > 0) + AND lcpb.installmentNumber IS NULL + """) + List findChargePaidByMappingsWithoutInstallmentNumber(@Param("loan") Loan loan); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java index c4dc6bd60..dad4c1492 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepository.java @@ -18,12 +18,16 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Set; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.portfolio.loanaccount.data.CumulativeIncomeFromIncomePosting; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData; +import org.apache.fineract.portfolio.loanaccount.data.TransactionPortionsForForeclosure; import org.apache.fineract.portfolio.loanaccount.data.UnpaidChargeData; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; @@ -32,31 +36,8 @@ import org.springframework.data.repository.query.Param; public interface LoanTransactionRepository extends JpaRepository, JpaSpecificationExecutor { - String FIND_ID_BY_EXTERNAL_ID = "SELECT lt.id FROM LoanTransaction lt WHERE lt.externalId = :externalId"; - String FIND_LOAN_ID_BY_ID = "SELECT lt.loan.id FROM LoanTransaction lt WHERE lt.id = :id"; - Optional findByIdAndLoanId(Long transactionId, Long loanId); - @Query(""" - SELECT lt FROM LoanTransaction lt - WHERE lt.loan.id = :loanId - AND lt.typeOf IN :transactionTypes - AND lt.id NOT IN :existingTransactionIds - AND lt.reversed = false - """) - List findNewTransactionsByLoanIdAndTypeAndNotInIds(@Param("loanId") Long loanId, - @Param("transactionTypes") List transactionTypes, - @Param("existingTransactionIds") List existingTransactionIds); - - @Query(""" - SELECT lt FROM LoanTransaction lt - WHERE lt.loan.id = :loanId - AND lt.typeOf IN :transactionTypes - AND lt.reversed = false - """) - List findNewTransactionsByLoanIdAndType(@Param("loanId") Long loanId, - @Param("transactionTypes") List transactionTypes); - @Query(""" SELECT new org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData( lt.loan.id, @@ -72,7 +53,7 @@ public interface LoanTransactionRepository extends JpaRepository fetchLoanTransactionsByTypeAndLessOrEqualDate( @Param("transactionType") LoanTransactionType transactionType, @Param("businessDate") LocalDate businessDate); - @Query(FIND_ID_BY_EXTERNAL_ID) + @Query("SELECT lt.id FROM LoanTransaction lt WHERE lt.externalId = :externalId") Long findIdByExternalId(@Param("externalId") ExternalId externalId); @Query(""" @@ -88,9 +69,41 @@ public interface LoanTransactionRepository extends JpaRepository fetchTotalUnpaidChargesForLoan(@Param("loan") Loan loan); - @Query(FIND_LOAN_ID_BY_ID) + @Query("SELECT lt.loan.id FROM LoanTransaction lt WHERE lt.id = :id") Optional findLoanIdById(@Param("id") Long id); + @Query(""" + SELECT COALESCE(SUM(lt.unrecognizedIncomePortion), 0) + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WAIVE_INTEREST + AND lt.reversed = false + AND lt.dateOf <= :toDate + """) + BigDecimal findTotalUnrecognizedIncomeFromInterestWaiverByLoanAndDate(@Param("loan") Loan loan, @Param("toDate") LocalDate toDate); + + @Query(""" + SELECT COALESCE(SUM(CASE WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL THEN lt.interestPortion + WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT THEN -lt.interestPortion + ELSE 0 END), 0) + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + """) + BigDecimal findTotalInterestAccruedAmount(@Param("loan") Loan loan); + + @Query(""" + SELECT COALESCE(SUM(lt.interestPortion), 0) + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL + AND lt.dateOf > :fromDate + AND lt.dateOf <= :dueDate + """) + BigDecimal findAccrualInterestInPeriod(@Param("loan") Loan loan, @Param("fromDate") LocalDate fromDate, + @Param("dueDate") LocalDate dueDate); + @Query(""" SELECT CASE WHEN COUNT(lt) > 0 THEN false ELSE true END FROM LoanTransaction lt @@ -133,4 +146,163 @@ public interface LoanTransactionRepository extends JpaRepository findReversedTransactionIdsByLoan(@Param("loan") Loan loan); + @Query(""" + SELECT + lt.typeOf AS transactionType, + lt.interestPortion AS interestPortion, + lt.feeChargesPortion AS feeChargesPortion, + lt.penaltyChargesPortion AS penaltyChargesPortion + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.dateOf <= :tillDate + AND lt.typeOf NOT IN ( + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DISBURSEMENT, + org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT_AT_DISBURSEMENT + ) + ORDER BY lt.dateOf + """) + List findTransactionDataForForeclosureIncome(@Param("loan") Loan loan, + @Param("tillDate") LocalDate tillDate); + + @Query(""" + SELECT lt + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf IN :types + AND lt.dateOf = :transactionDate + """) + Optional findNonReversedByLoanAndTypesAndDate(@Param("loan") Loan loan, @Param("type") Set types, + @Param("transactionDate") LocalDate transactionDate); + + @Query(""" + SELECT lt + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf IN :types + AND lt.dateOf > :transactionDate + ORDER BY lt.dateOf + """) + List findNonReversedByLoanAndTypesAndAfterDate(@Param("loan") Loan loan, + @Param("types") Set types, @Param("transactionDate") LocalDate transactionDate); + + @Query(""" + SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf IN :types + AND lt.dateOf >= :accrualDate + """) + boolean existsNonReversedByLoanAndTypesAndOnOrAfterDate(@Param("loan") Loan loan, @Param("types") Set types, + @Param("accrualDate") LocalDate accrualDate); + + @Query(""" + SELECT lt + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf IN :types + AND lt.id NOT IN :existingTransactionIds + """) + List findNonReversedByLoanAndTypesAndNotInIds(@Param("loan") Loan loan, @Param("types") Set types, + @Param("existingTransactionIds") List existingTransactionIds); + + @Query(""" + SELECT lt + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf IN :types + """) + List findNonReversedByLoanAndTypes(@Param("loan") Loan loan, @Param("types") Set types); + + @Query(""" + SELECT lt + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.dateOf >= :date + AND lt.typeOf IN :types + ORDER BY lt.dateOf + """) + List findNonReversedByLoanAndTypesAndOnOrAfterDate(@Param("loan") Loan loan, + @Param("types") Set types, @Param("date") LocalDate date); + + @Query(""" + SELECT new org.apache.fineract.portfolio.loanaccount.data.CumulativeIncomeFromIncomePosting( + COALESCE(SUM(lt.interestPortion), 0), + COALESCE(SUM(lt.feeChargesPortion), 0), + COALESCE(SUM(lt.penaltyChargesPortion), 0) + ) + FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INCOME_POSTING + """) + CumulativeIncomeFromIncomePosting findCumulativeIncomeByLoanAndType(@Param("loan") Loan loan); + + @Query(""" + SELECT COALESCE(SUM(CASE WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL THEN lcpb.amount + WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT THEN -lcpb.amount + ELSE 0 END), 0) + FROM LoanChargePaidBy lcpb + JOIN lcpb.loanTransaction lt + WHERE lcpb.loanCharge = :loanCharge + AND lt.reversed = false + """) + BigDecimal findChargeAccrualAmount(@Param("loanCharge") LoanCharge loanCharge); + + @Query(""" + SELECT COALESCE(SUM(lt.unrecognizedIncomePortion), 0) + FROM LoanChargePaidBy lcpb + JOIN lcpb.loanTransaction lt + WHERE lcpb.loanCharge = :loanCharge + AND lt.reversed = false + AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WAIVE_CHARGES + AND lt.dateOf <= :tillDate + """) + BigDecimal findChargeUnrecognizedWaivedAmount(@Param("loanCharge") LoanCharge loanCharge, @Param("tillDate") LocalDate tillDate); + + @Query(""" + SELECT MAX(lt.dateOf) FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + AND lt.typeOf IN :types + """) + Optional findLastNonReversedTransactionDateByLoanAndTypes(@Param("loan") Loan loan, + @Param("types") Set 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 findTransactionsForAccountingBridge(@Param("loan") Loan loan, + @Param("existingTransactionIds") List existingTransactionIds, + @Param("existingReversedTransactionIds") List 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 findTransactionsForAccountingBridge(@Param("loan") Loan loan, + @Param("existingTransactionIds") List existingTransactionIds); + + @Query(""" + SELECT lt FROM LoanTransaction lt + WHERE lt.loan = :loan + AND lt.reversed = false + """) + List findNonReversedTransactionsByLoan(@Param("loan") Loan loan); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java index f840e5bab..16da88fcf 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java @@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.springframework.stereotype.Component; @@ -42,6 +43,8 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class LoanAccountingBridgeMapper { + private final LoanTransactionRepository loanTransactionRepository; + public List deriveAccountingBridgeDataForChargeOff(final String currencyCode, final List existingTransactionIds, final List existingReversedTransactionIds, final boolean isAccountTransfer, final Loan loan) { @@ -72,16 +75,20 @@ public class LoanAccountingBridgeMapper { public AccountingBridgeDataDTO deriveAccountingBridgeData(final String currencyCode, final List existingTransactionIds, final List existingReversedTransactionIds, final boolean isAccountTransfer, final Loan loan) { - final List newLoanTransactions = new ArrayList<>(); - for (final LoanTransaction transaction : loan.getLoanTransactions()) { - if (transaction.isReversed() && existingTransactionIds.contains(transaction.getId()) - && !existingReversedTransactionIds.contains(transaction.getId())) { - newLoanTransactions.add(mapToLoanTransactionData(transaction, currencyCode)); - } else if (!existingTransactionIds.contains(transaction.getId())) { - newLoanTransactions.add(mapToLoanTransactionData(transaction, currencyCode)); - } + List transactions; + if (existingTransactionIds == null || existingTransactionIds.isEmpty()) { + transactions = loanTransactionRepository.findNonReversedTransactionsByLoan(loan); + } else if (existingReversedTransactionIds == null || existingReversedTransactionIds.isEmpty()) { + transactions = loanTransactionRepository.findTransactionsForAccountingBridge(loan, existingTransactionIds); + } else { + transactions = loanTransactionRepository.findTransactionsForAccountingBridge(loan, existingTransactionIds, + existingReversedTransactionIds); } + final List newLoanTransactions = transactions.stream() // + .map(transaction -> mapToLoanTransactionData(transaction, currencyCode)) // + .toList(); + return new AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(), currencyCode, loan.getSummary().getTotalInterestCharged(), loan.isCashBasedAccountingEnabledOnLoanProduct(), loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(), diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualTransactionBusinessEventService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualTransactionBusinessEventService.java index 36cfa4dfd..9723246ab 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualTransactionBusinessEventService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualTransactionBusinessEventService.java @@ -24,4 +24,5 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; public interface LoanAccrualTransactionBusinessEventService { void raiseBusinessEventForAccrualTransactions(Loan loan, List existingTransactionIds); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualTransactionBusinessEventServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualTransactionBusinessEventServiceImpl.java index 608d98a2a..ada8264fb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualTransactionBusinessEventServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualTransactionBusinessEventServiceImpl.java @@ -18,7 +18,11 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT; + import java.util.List; +import java.util.Set; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; @@ -37,17 +41,17 @@ public class LoanAccrualTransactionBusinessEventServiceImpl implements LoanAccru @Override public void raiseBusinessEventForAccrualTransactions(final Loan loan, final List existingTransactionIds) { - final List transactionTypes = List.of(LoanTransactionType.ACCRUAL, LoanTransactionType.ACCRUAL_ADJUSTMENT); - final List transactions = existingTransactionIds.isEmpty() - ? loanTransactionRepository.findNewTransactionsByLoanIdAndType(loan.getId(), transactionTypes) - : loanTransactionRepository.findNewTransactionsByLoanIdAndTypeAndNotInIds(loan.getId(), transactionTypes, - existingTransactionIds); + final Set accrualTypes = Set.of(ACCRUAL, ACCRUAL_ADJUSTMENT); + final List accrualTransactions = existingTransactionIds.isEmpty() + ? loanTransactionRepository.findNonReversedByLoanAndTypes(loan, accrualTypes) + : loanTransactionRepository.findNonReversedByLoanAndTypesAndNotInIds(loan, accrualTypes, existingTransactionIds); - for (final LoanTransaction transaction : transactions) { + accrualTransactions.forEach(transaction -> { final LoanTransactionBusinessEvent businessEvent = transaction.isAccrual() ? new LoanAccrualTransactionCreatedBusinessEvent(transaction) : new LoanAccrualAdjustmentTransactionBusinessEvent(transaction); businessEventNotifierService.notifyPostBusinessEvent(businessEvent); - } + }); } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java index ef944e9fe..d5b89f8bc 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingService.java @@ -28,11 +28,11 @@ import org.springframework.lang.NonNull; public interface LoanAccrualsProcessingService { - void addPeriodicAccruals(@NotNull LocalDate tilldate) throws MultiException; + void addPeriodicAccruals(@NotNull LocalDate tillDate) throws MultiException; - void addPeriodicAccruals(@NotNull LocalDate tilldate, @NotNull Loan loan) throws MultiException; + void addPeriodicAccruals(@NotNull LocalDate tillDate, @NotNull Loan loan) throws MultiException; - void addAccruals(@NotNull LocalDate tilldate) throws MultiException; + void addAccruals(@NotNull LocalDate tillDate) throws MultiException; void reprocessExistingAccruals(@NotNull Loan loan); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionReadService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionReadService.java index 6f6d7fcb1..bc7507b10 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionReadService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionReadService.java @@ -30,7 +30,6 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import java.util.ArrayList; import java.util.List; -import lombok.RequiredArgsConstructor; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; @@ -39,7 +38,6 @@ import org.springframework.transaction.annotation.Transactional; @Component @Transactional(readOnly = true) -@RequiredArgsConstructor public class LoanTransactionReadService { @PersistenceContext diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java index 72dc168a0..ef4b06310 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/addaccrualentries/AddAccrualEntriesTasklet.java @@ -25,11 +25,11 @@ import org.apache.fineract.infrastructure.core.exception.MultiException; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; -import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; @Slf4j @@ -37,11 +37,10 @@ import org.springframework.stereotype.Component; @Component public class AddAccrualEntriesTasklet implements Tasklet { - private final LoanReadPlatformService loanReadPlatformService; private final LoanAccrualsProcessingService loanAccrualsProcessingService; @Override - public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + public RepeatStatus execute(@NonNull final StepContribution contribution, @NonNull final ChunkContext chunkContext) throws Exception { try { addAccruals(DateUtils.getBusinessLocalDate()); } catch (MultiException e) { @@ -50,7 +49,7 @@ public class AddAccrualEntriesTasklet implements Tasklet { return RepeatStatus.FINISHED; } - private void addAccruals(final LocalDate tilldate) throws MultiException { - loanAccrualsProcessingService.addAccruals(tilldate); + private void addAccruals(final LocalDate tillDate) throws MultiException { + loanAccrualsProcessingService.addAccruals(tillDate); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index e80399092..e38eae466 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -24,6 +24,9 @@ import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentSche import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod; import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrualAdjustment; import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrueTransaction; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INCOME_POSTING; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; @@ -34,6 +37,7 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.Future; import java.util.function.Predicate; @@ -65,9 +69,12 @@ import org.apache.fineract.portfolio.loanaccount.data.AccrualBalances; import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData; import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData; import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodsData; +import org.apache.fineract.portfolio.loanaccount.data.CumulativeIncomeFromIncomePosting; +import org.apache.fineract.portfolio.loanaccount.data.TransactionPortionsForForeclosure; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidByRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails; @@ -77,6 +84,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; @@ -95,8 +103,7 @@ import org.springframework.transaction.support.TransactionTemplate; @RequiredArgsConstructor public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessingService { - private static final Predicate ACCRUAL_PREDICATE = t -> t.isNotReversed() - && (t.isAccrual() || t.isAccrualAdjustment()); + private static final Set ACCRUAL_TYPES = Set.of(ACCRUAL, ACCRUAL_ADJUSTMENT); private static final String ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE = "submitted-date"; private final ExternalIdFactory externalIdFactory; @@ -114,6 +121,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanChargeService loanChargeService; private final LoanBalanceService loanBalanceService; + private final LoanChargePaidByRepository loanChargePaidByRepository; /** * method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan @@ -267,21 +275,18 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } LocalDate lastCompoundingDate = loan.getDisbursementDate(); List compoundingDetails = extractInterestRecalculationAdditionalDetails(loan); - List incomeTransactions = retrieveListOfIncomePostingTransactions(loan); - List accrualTransactions = retrieveListOfAccrualTransactions(loan); for (LoanInterestRecalcualtionAdditionalDetails compoundingDetail : compoundingDetails) { if (!DateUtils.isBeforeBusinessDate(compoundingDetail.getEffectiveDate())) { break; } - LoanTransaction incomeTransaction = getTransactionForDate(incomeTransactions, compoundingDetail.getEffectiveDate()); - LoanTransaction accrualTransaction = getTransactionForDate(accrualTransactions, compoundingDetail.getEffectiveDate()); - addUpdateIncomeAndAccrualTransaction(loan, compoundingDetail, lastCompoundingDate, incomeTransaction, accrualTransaction); + + addUpdateIncomeAndAccrualTransaction(loan, compoundingDetail, lastCompoundingDate); lastCompoundingDate = compoundingDetail.getEffectiveDate(); } List installments = loan.getRepaymentScheduleInstallments(); LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment.getLastNonDownPaymentInstallment(installments); - reverseTransactionsAfter(incomeTransactions, lastInstallment.getDueDate(), false); - reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate(), false); + + reverseTransactionsAfter(loan, Set.of(ACCRUAL, ACCRUAL_ADJUSTMENT, INCOME_POSTING), lastInstallment.getDueDate(), false); } /** @@ -310,16 +315,14 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { final LoanRepaymentScheduleInstallment foreCloseDetail = loanBalanceService.fetchLoanForeclosureDetail(loan, foreClosureDate); MonetaryCurrency currency = loan.getCurrency(); - reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate, false); + reverseTransactionsAfter(loan, ACCRUAL_TYPES, foreClosureDate, false); - HashMap incomeDetails = new HashMap<>(); + final Map incomeDetails = determineReceivableIncomeForeClosure(loan, foreClosureDate); - determineReceivableIncomeForeClosure(loan, foreClosureDate, incomeDetails); - - Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus((Money) incomeDetails.get(Loan.INTEREST)); - Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus((Money) incomeDetails.get(Loan.FEE)); - Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus((Money) incomeDetails.get(Loan.PENALTIES)); - Money total = interestPortion.plus(feePortion).plus(penaltyPortion); + final Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus(incomeDetails.get(Loan.INTEREST)); + final Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus(incomeDetails.get(Loan.FEE)); + final Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus(incomeDetails.get(Loan.PENALTIES)); + final Money total = interestPortion.plus(feePortion).plus(penaltyPortion); if (total.isGreaterThanZero()) { createAccrualTransactionAndUpdateChargesPaidBy(loan, foreClosureDate, newAccrualTransactions, currency, interestPortion, @@ -341,10 +344,10 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing if (recalculationDetails != null && recalculationDetails.isCompoundingToBePostedAsTransaction()) { return; } - final List existingAccruals = retrieveListOfAccrualTransactions(loan); + final LocalDate lastDueDate = loan.getLastLoanRepaymentScheduleInstallment().getDueDate(); - reverseTransactionsAfter(existingAccruals, lastDueDate, addJournal); - ensureAccrualTransactionMappings(loan, existingAccruals, chargeOnDueDate); + reverseTransactionsAfter(loan, ACCRUAL_TYPES, lastDueDate, addJournal); + ensureAccrualTransactionMappings(loan, chargeOnDueDate); if (DateUtils.isAfter(tillDate, lastDueDate)) { tillDate = lastDueDate; } @@ -358,8 +361,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing : tillDate; if (progressiveAccrual && accruedTill != null && !DateUtils.isAfter(tillDate, accruedTill)) { if (isFinal) { - reverseTransactionsAfter(existingAccruals, accrualDate, addJournal); - } else if (isExistAccrualAfterDate(existingAccruals, accrualDate) && hasNoActiveChargeOnDate(loan, accrualDate)) { + reverseTransactionsAfter(loan, ACCRUAL_TYPES, accrualDate, addJournal); + } else if (loanTransactionRepository.existsNonReversedByLoanAndTypesAndOnOrAfterDate(loan, ACCRUAL_TYPES, accrualDate) + && hasNoActiveChargeOnDate(loan, accrualDate)) { return; } } @@ -446,10 +450,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } - private boolean isExistAccrualAfterDate(List existingAccruals, LocalDate accrualDate) { - return existingAccruals.stream().anyMatch(t -> !t.isReversed() && !DateUtils.isBefore(t.getDateOf(), accrualDate)); - } - private boolean hasNoActiveChargeOnDate(Loan loan, LocalDate accrualDate) { return loan.getLoanCharges(t -> t.isActive() && DateUtils.isEqual(t.getDueDate(), accrualDate)).isEmpty(); } @@ -540,11 +540,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing // unrecognized amount of the transaction is not mapped to installments LocalDate dueDate = installment.getDueDate(); LocalDate toDate = DateUtils.isBefore(dueDate, tillDate) ? dueDate : tillDate; - Predicate transactionPredicate = t -> !t.isReversed() && t.isInterestWaiver() - && !DateUtils.isAfter(t.getTransactionDate(), toDate); Loan loan = installment.getLoan(); - BigDecimal totalUnrecognized = loan.getLoanTransactions().stream().filter(transactionPredicate) - .map(LoanTransaction::getUnrecognizedIncomePortion).reduce(BigDecimal.ZERO, MathUtil::add); + BigDecimal totalUnrecognized = loanTransactionRepository.findTotalUnrecognizedIncomeFromInterestWaiverByLoanAndDate(loan, toDate); // total unrecognized amount from previous periods BigDecimal prevUnrecognized = accrualPeriods.getPeriods().stream() .filter(p -> p.getInstallmentNumber() < installment.getInstallmentNumber()) @@ -558,9 +555,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing @NotNull AccrualPeriodsData accrualPeriods, @NotNull LocalDate tillDate) { Loan loan = installment.getLoan(); if (isProgressiveAccrual(loan)) { - BigDecimal totalAccrued = loan.getLoanTransactions().stream().filter(ACCRUAL_PREDICATE) - .map(t -> t.isAccrual() ? t.getInterestPortion() : MathUtil.negate(t.getInterestPortion())) - .reduce(BigDecimal.ZERO, MathUtil::add); + BigDecimal totalAccrued = loanTransactionRepository.findTotalInterestAccruedAmount(loan); BigDecimal prevAccrued = accrualPeriods.getPeriods().stream() .filter(p -> p.getInstallmentNumber() < installment.getInstallmentNumber()) .map(p -> MathUtil.toBigDecimal(p.getTransactionAccrued())).reduce(BigDecimal.ZERO, MathUtil::add); @@ -569,9 +564,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing return isInPeriod(tillDate, installment, false) ? accrued : MathUtil.min(installment.getInterestAccrued(), accrued, false); } else { return isFullPeriod(tillDate, installment) ? installment.getInterestAccrued() - : loan.getLoanTransactions().stream() - .filter(t -> !t.isReversed() && t.isAccrual() && isInPeriod(t.getTransactionDate(), installment, false)) - .map(LoanTransaction::getInterestPortion).reduce(BigDecimal.ZERO, MathUtil::add); + : loanTransactionRepository.findAccrualInterestInPeriod(loan, installment.getFromDate(), installment.getDueDate()); } } @@ -624,9 +617,10 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing .setChargeAmount(chargeAmount); chargeData.setChargeAccruable(MathUtil.minusToZero(chargeAmount, waived)); - final Money unrecognizedWaived = MathUtil.toMoney(calcChargeUnrecognizedWaivedAmount(paidBys, tillDate), currency); + final Money unrecognizedWaived = MathUtil + .toMoney(loanTransactionRepository.findChargeUnrecognizedWaivedAmount(loanCharge, tillDate), currency); final Money transactionWaived = MathUtil.minusToZero(waived, unrecognizedWaived); - final Money transactionAccrued = MathUtil.toMoney(calcChargeAccruedAmount(paidBys), currency); + final Money transactionAccrued = MathUtil.toMoney(loanTransactionRepository.findChargeAccrualAmount(loanCharge), currency); chargeData.setTransactionAccrued(transactionAccrued); chargeData.setChargeAccrued(MathUtil.minusToZero(transactionAccrued, transactionWaived)); @@ -642,22 +636,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing }).map(LoanChargePaidBy::getAmount).reduce(BigDecimal.ZERO, MathUtil::add); } - @NotNull - private BigDecimal calcChargeUnrecognizedWaivedAmount(@NotNull final Collection loanChargePaidBy, - @NotNull final LocalDate tillDate) { - return loanChargePaidBy.stream().filter(pb -> { - final LoanTransaction t = pb.getLoanTransaction(); - return !t.isReversed() && t.isWaiveCharge() && !DateUtils.isAfter(t.getTransactionDate(), tillDate); - }).map(pb -> pb.getLoanTransaction().getUnrecognizedIncomePortion()).reduce(BigDecimal.ZERO, MathUtil::add); - } - - @NotNull - private BigDecimal calcChargeAccruedAmount(@NotNull final Collection loanChargePaidBy) { - return loanChargePaidBy.stream().filter(pb -> ACCRUAL_PREDICATE.test(pb.getLoanTransaction())) - .map(pb -> pb.getLoanTransaction().isAccrual() ? pb.getAmount() : MathUtil.negate(pb.getAmount())) - .reduce(BigDecimal.ZERO, MathUtil::add); - } - private boolean isChargeDue(@NotNull final LoanCharge loanCharge, @NotNull final LocalDate tillDate, boolean chargeOnDueDate, final LoanRepaymentScheduleInstallment installment, final boolean isFirstPeriod) { final LocalDate fromDate = installment.getFromDate(); @@ -697,10 +675,11 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing LoanTransaction transaction = adjustment ? accrualAdjustment(loan, loan.getOffice(), transactionDate, amount, interest, fee, penalty, externalIdFactory.create()) : accrueTransaction(loan, loan.getOffice(), transactionDate, amount, interest, fee, penalty, externalIdFactory.create()); - loan.addLoanTransaction(transaction); // update repayment schedule portions addTransactionMappings(transaction, accrualPeriod, adjustment); + loanTransactionRepository.save(transaction); + return transaction; } @@ -767,7 +746,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing return; } final boolean isChargeOnDueDate = isChargeOnDueDate(); - ensureAccrualTransactionMappings(loan, accrualTransactions, isChargeOnDueDate); + ensureAccrualTransactionMappings(loan, isChargeOnDueDate); LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); LocalDate lastDueDate = lastInstallment.getDueDate(); if (isProgressiveAccrual(loan)) { @@ -826,7 +805,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } // reverse accruals after last installment - reverseTransactionsAfter(accrualTransactions, lastDueDate, false); + reverseTransactionsAfter(loan, ACCRUAL_TYPES, lastDueDate, false); } private void reprocessNonPeriodicAccruals(Loan loan, final List accrualTransactions) { @@ -846,7 +825,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } final LoanTransaction interestAccrualTransaction = LoanTransaction.accrueInterest(loan.getOffice(), loan, interestApplied, loan.getDisbursementDate(), externalId); - loan.addLoanTransaction(interestAccrualTransaction); + loanTransactionRepository.save(interestAccrualTransaction); } } else { Set chargePaidBies = accrualTransaction.getLoanChargesPaid(); @@ -922,7 +901,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } private void addUpdateIncomeAndAccrualTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, - LocalDate lastCompoundingDate, LoanTransaction existingIncomeTransaction, LoanTransaction existingAccrualTransaction) { + LocalDate lastCompoundingDate) { BigDecimal interest = BigDecimal.ZERO; BigDecimal fee = BigDecimal.ZERO; BigDecimal penalties = BigDecimal.ZERO; @@ -949,43 +928,43 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing externalId = ExternalId.generate(); } - createUpdateIncomePostingTransaction(loan, compoundingDetail, existingIncomeTransaction, interest, fee, penalties, externalId); - createUpdateAccrualTransaction(loan, compoundingDetail, existingAccrualTransaction, interest, fee, penalties, feeDetails, - externalId); + createUpdateIncomePostingTransaction(loan, compoundingDetail, interest, fee, penalties, externalId); + createUpdateAccrualTransaction(loan, compoundingDetail, interest, fee, penalties, feeDetails, externalId); loan.updateLoanOutstandingBalances(); } private void createUpdateIncomePostingTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, - LoanTransaction existingIncomeTransaction, BigDecimal interest, BigDecimal fee, BigDecimal penalties, ExternalId externalId) { - if (existingIncomeTransaction == null) { + BigDecimal interest, BigDecimal fee, BigDecimal penalties, ExternalId externalId) { + final Optional incomeTransaction = loanTransactionRepository.findNonReversedByLoanAndTypesAndDate(loan, + Set.of(INCOME_POSTING), compoundingDetail.getEffectiveDate()); + if (incomeTransaction.isEmpty()) { LoanTransaction transaction = LoanTransaction.incomePosting(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), compoundingDetail.getAmount(), interest, fee, penalties, externalId); - loan.addLoanTransaction(transaction); - } else if (existingIncomeTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { - existingIncomeTransaction.reverse(); + loanTransactionRepository.save(transaction); + } else if (incomeTransaction.get().getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { + incomeTransaction.get().reverse(); LoanTransaction transaction = LoanTransaction.incomePosting(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), compoundingDetail.getAmount(), interest, fee, penalties, externalId); - loan.addLoanTransaction(transaction); + loanTransactionRepository.save(transaction); } } private void createUpdateAccrualTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail, - LoanTransaction existingAccrualTransaction, BigDecimal interest, BigDecimal fee, BigDecimal penalties, - HashMap feeDetails, ExternalId externalId) { + BigDecimal interest, BigDecimal fee, BigDecimal penalties, HashMap feeDetails, ExternalId externalId) { if (configurationDomainService.isExternalIdAutoGenerationEnabled()) { externalId = ExternalId.generate(); } if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - if (existingAccrualTransaction == null - || !MathUtil.isEqualTo(existingAccrualTransaction.getAmount(), compoundingDetail.getAmount())) { - if (existingAccrualTransaction != null) { - existingAccrualTransaction.reverse(); - } + final Optional accrualTransaction = loanTransactionRepository.findNonReversedByLoanAndTypesAndDate(loan, + Set.of(LoanTransactionType.ACCRUAL, LoanTransactionType.ACCRUAL_ADJUSTMENT), compoundingDetail.getEffectiveDate()); + + if (accrualTransaction.isEmpty() || !MathUtil.isEqualTo(accrualTransaction.get().getAmount(), compoundingDetail.getAmount())) { + accrualTransaction.ifPresent(LoanTransaction::reverse); LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), compoundingDetail.getAmount(), interest, fee, penalties, externalId); updateLoanChargesPaidBy(loan, accrual, feeDetails, null); - loan.addLoanTransaction(accrual); + loanTransactionRepository.save(accrual); } } } @@ -999,27 +978,29 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing && loan.getStatus().isClosedObligationsMet() && !loan.isNpa() && !loan.isChargedOff()) { LocalDate closedDate = loan.getClosedOnDate(); - reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(loan), closedDate); - reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(loan), closedDate); + reverseTransactionsOnOrAfter(loan, Set.of(INCOME_POSTING, ACCRUAL, ACCRUAL_ADJUSTMENT), closedDate); - HashMap cumulativeIncomeFromInstallments = new HashMap<>(); + final Map cumulativeIncomeFromInstallments = new HashMap<>(); determineCumulativeIncomeFromInstallments(loan, cumulativeIncomeFromInstallments); - HashMap cumulativeIncomeFromIncomePosting = new HashMap<>(); - determineCumulativeIncomeDetails(loan, retrieveListOfIncomePostingTransactions(loan), cumulativeIncomeFromIncomePosting); - BigDecimal interestToPost = cumulativeIncomeFromInstallments.get(Loan.INTEREST) - .subtract(cumulativeIncomeFromIncomePosting.get(Loan.INTEREST)); - BigDecimal feeToPost = cumulativeIncomeFromInstallments.get(Loan.FEE).subtract(cumulativeIncomeFromIncomePosting.get(Loan.FEE)); - BigDecimal penaltyToPost = cumulativeIncomeFromInstallments.get(Loan.PENALTY) - .subtract(cumulativeIncomeFromIncomePosting.get(Loan.PENALTY)); - BigDecimal amountToPost = interestToPost.add(feeToPost).add(penaltyToPost); + final CumulativeIncomeFromIncomePosting cumulativeIncomeFromIncomePosting = loanTransactionRepository + .findCumulativeIncomeByLoanAndType(loan); + + final BigDecimal interestToPost = cumulativeIncomeFromInstallments.get(Loan.INTEREST) + .subtract(cumulativeIncomeFromIncomePosting.interestAmount()); + final BigDecimal feeToPost = cumulativeIncomeFromInstallments.get(Loan.FEE) + .subtract(cumulativeIncomeFromIncomePosting.feeAmount()); + final BigDecimal penaltyToPost = cumulativeIncomeFromInstallments.get(Loan.PENALTY) + .subtract(cumulativeIncomeFromIncomePosting.penaltyAmount()); + final BigDecimal amountToPost = interestToPost.add(feeToPost).add(penaltyToPost); createIncomePostingAndAccrualTransactionOnLoanClosure(loan, closedDate, interestToPost, feeToPost, penaltyToPost, amountToPost); } loan.updateLoanOutstandingBalances(); } - private void determineCumulativeIncomeFromInstallments(Loan loan, HashMap cumulativeIncomeFromInstallments) { + private void determineCumulativeIncomeFromInstallments(final Loan loan, + final Map cumulativeIncomeFromInstallments) { BigDecimal interest = BigDecimal.ZERO; BigDecimal fee = BigDecimal.ZERO; BigDecimal penalty = BigDecimal.ZERO; @@ -1034,21 +1015,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing cumulativeIncomeFromInstallments.put(Loan.PENALTY, penalty); } - private void determineCumulativeIncomeDetails(Loan loan, List transactions, - HashMap incomeDetailsMap) { - BigDecimal interest = BigDecimal.ZERO; - BigDecimal fee = BigDecimal.ZERO; - BigDecimal penalty = BigDecimal.ZERO; - for (LoanTransaction transaction : transactions) { - interest = interest.add(transaction.getInterestPortion(loan.getCurrency()).getAmount()); - fee = fee.add(transaction.getFeeChargesPortion(loan.getCurrency()).getAmount()); - penalty = penalty.add(transaction.getPenaltyChargesPortion(loan.getCurrency()).getAmount()); - } - incomeDetailsMap.put(Loan.INTEREST, interest); - incomeDetailsMap.put(Loan.FEE, fee); - incomeDetailsMap.put(Loan.PENALTY, penalty); - } - private void createIncomePostingAndAccrualTransactionOnLoanClosure(Loan loan, LocalDate closedDate, BigDecimal interestToPost, BigDecimal feeToPost, BigDecimal penaltyToPost, BigDecimal amountToPost) { ExternalId externalId = ExternalId.empty(); @@ -1059,14 +1025,13 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } LoanTransaction finalIncomeTransaction = LoanTransaction.incomePosting(loan, loan.getOffice(), closedDate, amountToPost, interestToPost, feeToPost, penaltyToPost, externalId); - loan.addLoanTransaction(finalIncomeTransaction); + loanTransactionRepository.save(finalIncomeTransaction); if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - List updatedAccrualTransactions = retrieveListOfAccrualTransactions(loan); - LocalDate lastAccruedDate = loan.getDisbursementDate(); - if (!updatedAccrualTransactions.isEmpty()) { - lastAccruedDate = updatedAccrualTransactions.get(updatedAccrualTransactions.size() - 1).getTransactionDate(); - } + final LocalDate lastAccruedDate = loanTransactionRepository + .findLastNonReversedTransactionDateByLoanAndTypes(loan, ACCRUAL_TYPES) // + .orElse(loan.getDisbursementDate()); + HashMap feeDetails = new HashMap<>(); determineFeeDetails(loan, lastAccruedDate, closedDate, feeDetails); if (isExternalIdAutoGenerationEnabled) { @@ -1075,30 +1040,35 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing LoanTransaction finalAccrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), closedDate, amountToPost, interestToPost, feeToPost, penaltyToPost, externalId); updateLoanChargesPaidBy(loan, finalAccrual, feeDetails, null); - loan.addLoanTransaction(finalAccrual); + loanTransactionRepository.save(finalAccrual); } } - // LoanForClosure - - private void determineReceivableIncomeForeClosure(Loan loan, final LocalDate tillDate, Map incomeDetails) { + private Map determineReceivableIncomeForeClosure(final Loan loan, final LocalDate tillDate) { MonetaryCurrency currency = loan.getCurrency(); Money receivableInterest = Money.zero(currency); Money receivableFee = Money.zero(currency); Money receivablePenalty = Money.zero(currency); - for (final LoanTransaction transaction : loan.getLoanTransactions()) { - if (transaction.isNotReversed() && !transaction.isRepaymentAtDisbursement() && !transaction.isDisbursement() - && !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) { - if (transaction.isAccrual()) { - receivableInterest = receivableInterest.plus(transaction.getInterestPortion(currency)); - receivableFee = receivableFee.plus(transaction.getFeeChargesPortion(currency)); - receivablePenalty = receivablePenalty.plus(transaction.getPenaltyChargesPortion(currency)); - } else if (transaction.isRepaymentLikeType() || transaction.isChargePayment() || transaction.isAccrualAdjustment()) { - receivableInterest = receivableInterest.minus(transaction.getInterestPortion(currency)); - receivableFee = receivableFee.minus(transaction.getFeeChargesPortion(currency)); - receivablePenalty = receivablePenalty.minus(transaction.getPenaltyChargesPortion(currency)); - } + + final List transactionPortions = loanTransactionRepository + .findTransactionDataForForeclosureIncome(loan, tillDate); + + for (TransactionPortionsForForeclosure transactionPortion : transactionPortions) { + LoanTransactionType transactionType = transactionPortion.getTransactionType(); + BigDecimal interestPortion = transactionPortion.getInterestPortion(); + BigDecimal feePortion = transactionPortion.getFeeChargesPortion(); + BigDecimal penaltyPortion = transactionPortion.getPenaltyChargesPortion(); + + if (transactionType.isAccrual()) { + receivableInterest = receivableInterest.plus(Money.of(currency, interestPortion)); + receivableFee = receivableFee.plus(Money.of(currency, feePortion)); + receivablePenalty = receivablePenalty.plus(Money.of(currency, penaltyPortion)); + } else if (transactionType.isRepayment() || transactionType.isChargePayment() || transactionType.isAccrualAdjustment()) { + receivableInterest = receivableInterest.minus(Money.of(currency, interestPortion)); + receivableFee = receivableFee.minus(Money.of(currency, feePortion)); + receivablePenalty = receivablePenalty.minus(Money.of(currency, penaltyPortion)); } + if (receivableInterest.isLessThanZero()) { receivableInterest = receivableInterest.zero(); } @@ -1110,9 +1080,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } - incomeDetails.put(Loan.INTEREST, receivableInterest); - incomeDetails.put(Loan.FEE, receivableFee); - incomeDetails.put(Loan.PENALTIES, receivablePenalty); + return Map.of(Loan.INTEREST, receivableInterest, Loan.FEE, receivableFee, Loan.PENALTIES, receivablePenalty); } private void createAccrualTransactionAndUpdateChargesPaidBy(Loan loan, LocalDate foreClosureDate, @@ -1126,7 +1094,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing fromDate = loan.getAccruedTill(); } newAccrualTransactions.add(accrualTransaction); - loan.addLoanTransaction(accrualTransaction); Set accrualCharges = accrualTransaction.getLoanChargesPaid(); for (LoanCharge loanCharge : loan.getActiveCharges()) { boolean isDue = loanCharge.isDueInPeriod(fromDate, foreClosureDate, DateUtils.isEqual(fromDate, loan.getDisbursementDate())); @@ -1148,50 +1115,32 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } - private void ensureAccrualTransactionMappings(final Loan loan, final List existingAccrualTransactions, - final boolean chargeOnDueDate) { - final List transactions = existingAccrualTransactions.stream() - .filter(t -> !MathUtil.isEmpty(t.getFeeChargesPortion()) || !MathUtil.isEmpty(t.getPenaltyChargesPortion())).toList(); + private void ensureAccrualTransactionMappings(final Loan loan, final boolean chargeOnDueDate) { + final List entriesToProcess = loanChargePaidByRepository.findChargePaidByMappingsWithoutInstallmentNumber(loan); - if (transactions.isEmpty()) { + if (entriesToProcess.isEmpty()) { return; } final int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()); - for (LoanTransaction transaction : transactions) { - for (LoanChargePaidBy paidBy : transaction.getLoanChargesPaid()) { - if (paidBy.getInstallmentNumber() == null) { - final LoanCharge loanCharge = paidBy.getLoanCharge(); - final LocalDate chargeDate = (chargeOnDueDate || loanCharge.isInstalmentFee()) ? transaction.getTransactionDate() - : loanCharge.getDueDate(); - final LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallment( - i -> isInPeriod(chargeDate, i, i.getInstallmentNumber().equals(firstInstallmentNumber))); - paidBy.setInstallmentNumber(installment.getInstallmentNumber()); - } + for (LoanChargePaidBy paidBy : entriesToProcess) { + final LoanCharge loanCharge = paidBy.getLoanCharge(); + final LocalDate chargeDate = (chargeOnDueDate || loanCharge.isInstalmentFee()) + ? paidBy.getLoanTransaction().getTransactionDate() + : loanCharge.getDueDate(); + final LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallment( + i -> isInPeriod(chargeDate, i, i.getInstallmentNumber().equals(firstInstallmentNumber))); + if (installment != null) { + paidBy.setInstallmentNumber(installment.getInstallmentNumber()); } } } private List retrieveListOfAccrualTransactions(final Loan loan) { - return loan.getLoanTransactions().stream().filter(ACCRUAL_PREDICATE).sorted(LoanTransactionComparator.INSTANCE) - .collect(Collectors.toList()); - } - - private List retrieveListOfIncomePostingTransactions(final Loan loan) { - return loan.getLoanTransactions().stream() // - .filter(transaction -> transaction.isNotReversed() && transaction.isIncomePosting()) // + return loanTransactionRepository.findNonReversedByLoanAndTypes(loan, ACCRUAL_TYPES).stream() .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } - private LoanTransaction getTransactionForDate(final List transactions, final LocalDate effectiveDate) { - for (LoanTransaction loanTransaction : transactions) { - if (DateUtils.isEqual(effectiveDate, loanTransaction.getTransactionDate())) { - return loanTransaction; - } - } - return null; - } - private boolean isChargeOnDueDate() { final String chargeAccrualDateType = configurationDomainService.getAccrualDateConfigForCharge(); return !ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE.equalsIgnoreCase(chargeAccrualDateType); @@ -1263,21 +1212,15 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing } } - private void reverseTransactionsAfter(final List accrualTransactions, final LocalDate effectiveDate, + private void reverseTransactionsAfter(final Loan loan, final Set types, final LocalDate effectiveDate, final boolean addEvent) { - for (LoanTransaction accrualTransaction : accrualTransactions) { - if (!accrualTransaction.isReversed() && DateUtils.isAfter(accrualTransaction.getTransactionDate(), effectiveDate)) { - reverseAccrual(accrualTransaction, addEvent); - } - } + loanTransactionRepository.findNonReversedByLoanAndTypesAndAfterDate(loan, types, effectiveDate) + .forEach(transaction -> reverseAccrual(transaction, addEvent)); } - private void reverseTransactionsOnOrAfter(final List accrualTransactions, final LocalDate date) { - for (LoanTransaction accrualTransaction : accrualTransactions) { - if (!accrualTransaction.isReversed() && !DateUtils.isBefore(accrualTransaction.getTransactionDate(), date)) { - reverseAccrual(accrualTransaction, false); - } - } + private void reverseTransactionsOnOrAfter(final Loan loan, final Set types, final LocalDate date) { + loanTransactionRepository.findNonReversedByLoanAndTypesAndOnOrAfterDate(loan, types, date) + .forEach(transaction -> reverseAccrual(transaction, false)); } private void reverseAccrual(final LoanTransaction transaction, final boolean addEvent) {