FINERACT-2274: Rework Accrual service

This commit is contained in:
Oleksii Novikov
2025-06-01 18:03:22 +02:00
committed by Adam Saghy
parent fa405f09fd
commit f6f8c6f4a5
11 changed files with 419 additions and 225 deletions
@@ -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) {
}
@@ -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();
}
@@ -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<LoanChargePaidBy, Long>, JpaSpecificationExecutor<LoanCharge> {
@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<LoanChargePaidBy> findChargePaidByMappingsWithoutInstallmentNumber(@Param("loan") Loan loan);
}
@@ -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<LoanTransaction, Long>, JpaSpecificationExecutor<LoanTransaction> {
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<LoanTransaction> 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<LoanTransaction> findNewTransactionsByLoanIdAndTypeAndNotInIds(@Param("loanId") Long loanId,
@Param("transactionTypes") List<LoanTransactionType> transactionTypes,
@Param("existingTransactionIds") List<Long> existingTransactionIds);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan.id = :loanId
AND lt.typeOf IN :transactionTypes
AND lt.reversed = false
""")
List<LoanTransaction> findNewTransactionsByLoanIdAndType(@Param("loanId") Long loanId,
@Param("transactionTypes") List<LoanTransactionType> transactionTypes);
@Query("""
SELECT new org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData(
lt.loan.id,
@@ -72,7 +53,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
Collection<LoanScheduleDelinquencyData> 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<LoanTransaction
""")
List<UnpaidChargeData> fetchTotalUnpaidChargesForLoan(@Param("loan") Loan loan);
@Query(FIND_LOAN_ID_BY_ID)
@Query("SELECT lt.loan.id FROM LoanTransaction lt WHERE lt.id = :id")
Optional<Long> 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<LoanTransaction
""")
List<Long> 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<TransactionPortionsForForeclosure> 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<LoanTransaction> findNonReversedByLoanAndTypesAndDate(@Param("loan") Loan loan, @Param("type") Set<LoanTransactionType> 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<LoanTransaction> findNonReversedByLoanAndTypesAndAfterDate(@Param("loan") Loan loan,
@Param("types") Set<LoanTransactionType> 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<LoanTransactionType> 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<LoanTransaction> findNonReversedByLoanAndTypesAndNotInIds(@Param("loan") Loan loan, @Param("types") Set<LoanTransactionType> types,
@Param("existingTransactionIds") List<Long> existingTransactionIds);
@Query("""
SELECT lt
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf IN :types
""")
List<LoanTransaction> findNonReversedByLoanAndTypes(@Param("loan") Loan loan, @Param("types") Set<LoanTransactionType> 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<LoanTransaction> findNonReversedByLoanAndTypesAndOnOrAfterDate(@Param("loan") Loan loan,
@Param("types") Set<LoanTransactionType> 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<LocalDate> findLastNonReversedTransactionDateByLoanAndTypes(@Param("loan") Loan loan,
@Param("types") Set<LoanTransactionType> types);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND (
(lt.reversed = true AND lt.id IN :existingTransactionIds AND lt.id NOT IN :existingReversedTransactionIds)
OR (lt.id NOT IN :existingTransactionIds)
)
""")
List<LoanTransaction> findTransactionsForAccountingBridge(@Param("loan") Loan loan,
@Param("existingTransactionIds") List<Long> existingTransactionIds,
@Param("existingReversedTransactionIds") List<Long> existingReversedTransactionIds);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND (
(lt.reversed = true AND lt.id IN :existingTransactionIds)
OR (lt.id NOT IN :existingTransactionIds)
)
""")
List<LoanTransaction> findTransactionsForAccountingBridge(@Param("loan") Loan loan,
@Param("existingTransactionIds") List<Long> existingTransactionIds);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
""")
List<LoanTransaction> findNonReversedTransactionsByLoan(@Param("loan") Loan loan);
}
@@ -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<AccountingBridgeDataDTO> deriveAccountingBridgeDataForChargeOff(final String currencyCode,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final boolean isAccountTransfer,
final Loan loan) {
@@ -72,16 +75,20 @@ public class LoanAccountingBridgeMapper {
public AccountingBridgeDataDTO deriveAccountingBridgeData(final String currencyCode, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final boolean isAccountTransfer, final Loan loan) {
final List<AccountingBridgeLoanTransactionDTO> 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<LoanTransaction> 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<AccountingBridgeLoanTransactionDTO> 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(),
@@ -24,4 +24,5 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public interface LoanAccrualTransactionBusinessEventService {
void raiseBusinessEventForAccrualTransactions(Loan loan, List<Long> existingTransactionIds);
}
@@ -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<Long> existingTransactionIds) {
final List<LoanTransactionType> transactionTypes = List.of(LoanTransactionType.ACCRUAL, LoanTransactionType.ACCRUAL_ADJUSTMENT);
final List<LoanTransaction> transactions = existingTransactionIds.isEmpty()
? loanTransactionRepository.findNewTransactionsByLoanIdAndType(loan.getId(), transactionTypes)
: loanTransactionRepository.findNewTransactionsByLoanIdAndTypeAndNotInIds(loan.getId(), transactionTypes,
existingTransactionIds);
final Set<LoanTransactionType> accrualTypes = Set.of(ACCRUAL, ACCRUAL_ADJUSTMENT);
final List<LoanTransaction> 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);
}
});
}
}
@@ -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);
@@ -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
@@ -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);
}
}
@@ -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<LoanTransaction> ACCRUAL_PREDICATE = t -> t.isNotReversed()
&& (t.isAccrual() || t.isAccrualAdjustment());
private static final Set<LoanTransactionType> 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<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = extractInterestRecalculationAdditionalDetails(loan);
List<LoanTransaction> incomeTransactions = retrieveListOfIncomePostingTransactions(loan);
List<LoanTransaction> 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<LoanRepaymentScheduleInstallment> 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<String, Object> incomeDetails = new HashMap<>();
final Map<String, Money> 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<LoanTransaction> 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<LoanTransaction> 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<LoanTransaction> 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> 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> 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<LoanTransaction> 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<LoanChargePaidBy> 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<LoanTransaction> 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<String, Object> feeDetails, ExternalId externalId) {
BigDecimal interest, BigDecimal fee, BigDecimal penalties, HashMap<String, Object> 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<LoanTransaction> 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<String, BigDecimal> cumulativeIncomeFromInstallments = new HashMap<>();
final Map<String, BigDecimal> cumulativeIncomeFromInstallments = new HashMap<>();
determineCumulativeIncomeFromInstallments(loan, cumulativeIncomeFromInstallments);
HashMap<String, BigDecimal> 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<String, BigDecimal> cumulativeIncomeFromInstallments) {
private void determineCumulativeIncomeFromInstallments(final Loan loan,
final Map<String, BigDecimal> 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<LoanTransaction> transactions,
HashMap<String, BigDecimal> 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<LoanTransaction> 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<String, Object> 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<String, Object> incomeDetails) {
private Map<String, Money> 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<TransactionPortionsForForeclosure> 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<LoanChargePaidBy> 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<LoanTransaction> existingAccrualTransactions,
final boolean chargeOnDueDate) {
final List<LoanTransaction> 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<LoanChargePaidBy> 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<LoanTransaction> retrieveListOfAccrualTransactions(final Loan loan) {
return loan.getLoanTransactions().stream().filter(ACCRUAL_PREDICATE).sorted(LoanTransactionComparator.INSTANCE)
.collect(Collectors.toList());
}
private List<LoanTransaction> 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<LoanTransaction> 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<LoanTransaction> accrualTransactions, final LocalDate effectiveDate,
private void reverseTransactionsAfter(final Loan loan, final Set<LoanTransactionType> 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<LoanTransaction> 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<LoanTransactionType> types, final LocalDate date) {
loanTransactionRepository.findNonReversedByLoanAndTypesAndOnOrAfterDate(loan, types, date)
.forEach(transaction -> reverseAccrual(transaction, false));
}
private void reverseAccrual(final LoanTransaction transaction, final boolean addEvent) {