FINERACT-2234: Replace in-memory transaction loops with repository queries

This commit is contained in:
Oleksii Novikov
2025-07-01 20:14:28 +02:00
committed by Adam Saghy
parent dd4ca4d3e2
commit cb6a15c3d2
29 changed files with 281 additions and 228 deletions
@@ -58,6 +58,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.Transactional;
@@ -77,6 +78,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
private final LoanDelinquencyActionRepository loanDelinquencyActionRepository;
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final ConfigurationDomainService configurationDomainService;
private final LoanTransactionRepository loanTransactionRepository;
@Override
public List<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
@@ -174,7 +176,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
private void addInstallmentLevelDelinquencyData(CollectionData collectionData, Long loanId) {
Collection<LoanInstallmentDelinquencyTagData> loanInstallmentDelinquencyTagData = retrieveLoanInstallmentsCurrentDelinquencyTag(
loanId);
if (loanInstallmentDelinquencyTagData != null && loanInstallmentDelinquencyTagData.size() > 0) {
if (loanInstallmentDelinquencyTagData != null && !loanInstallmentDelinquencyTagData.isEmpty()) {
// installment level delinquency grouped by rangeId, and summed up the delinquent amount
Collection<InstallmentLevelDelinquency> installmentLevelDelinquencies = loanInstallmentDelinquencyTagData.stream()
@@ -254,12 +256,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
}
}
LocalDate lastTransactionDate = null;
for (final LoanTransaction transaction : loan.getLoanTransactions()) {
if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) {
lastTransactionDate = transaction.getTransactionDate();
}
}
final LocalDate lastTransactionDate = loanTransactionRepository.findLastRepaymentLikeTransactionDate(loan).orElse(null);
LocalDate possibleNextRepaymentDate = earliestUnpaidInstallmentDate;
if (DateUtils.isAfter(lastTransactionDate, earliestUnpaidInstallmentDate)) {
@@ -42,6 +42,7 @@ import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketPars
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionReadService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -60,11 +61,12 @@ public class DelinquencyConfiguration {
LoanDelinquencyDomainService loanDelinquencyDomainService,
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag,
LoanDelinquencyActionRepository loanDelinquencyActionRepository,
DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, ConfigurationDomainService configurationDomainService) {
DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, ConfigurationDomainService configurationDomainService,
LoanTransactionRepository loanTransactionRepository) {
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, delinquencyEffectivePauseHelper,
configurationDomainService);
configurationDomainService, loanTransactionRepository);
}
@Bean
@@ -59,7 +59,6 @@ import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -74,7 +73,6 @@ import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
@@ -433,13 +431,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel,
final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) {
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency,
final LocalDate submittedOnDate) {
return new Loan(accountNo, client, null, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, collateral, null, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel,
enableInstallmentLevelDelinquency, submittedOnDate);
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency,
submittedOnDate);
}
public static Loan newGroupLoanApplication(final String accountNo, final Group group, final AccountType loanType,
@@ -450,13 +448,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel,
final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) {
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency,
final LocalDate submittedOnDate) {
return new Loan(accountNo, null, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel,
enableInstallmentLevelDelinquency, submittedOnDate);
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency,
submittedOnDate);
}
public static Loan newIndividualLoanApplicationFromGroup(final String accountNo, final Client client, final Group group,
@@ -467,13 +465,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel,
final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) {
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency,
final LocalDate submittedOnDate) {
return new Loan(accountNo, client, group, loanType, fund, officer, loanPurpose, transactionProcessingStrategy, loanProduct,
loanRepaymentScheduleDetail, null, loanCharges, null, syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel,
enableInstallmentLevelDelinquency, submittedOnDate);
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency,
submittedOnDate);
}
protected Loan() {
@@ -488,8 +486,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final List<LoanDisbursementDetails> disbursementDetails, final BigDecimal maxOutstandingLoanBalance,
final Boolean createStandingInstructionAtDisbursement, final Boolean isFloatingInterestRate,
final BigDecimal interestRateDifferential, final List<Rate> rates, final BigDecimal fixedPrincipalPercentagePerInstallment,
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final LoanScheduleModel loanScheduleModel,
final Boolean enableInstallmentLevelDelinquency, final LocalDate submittedOnDate) {
final ExternalId externalId, final LoanApplicationTerms loanApplicationTerms, final Boolean enableInstallmentLevelDelinquency,
final LocalDate submittedOnDate) {
this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail;
this.isFloatingInterestRate = isFloatingInterestRate;
@@ -850,17 +848,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.postDatedChecks = new ArrayList<>();
}
public List<LoanTransaction> retrieveListOfTransactionsExcludeAccruals() {
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isNotReversed() && !transaction.isNonMonetaryTransaction()) {
repaymentsOrWaivers.add(transaction);
}
}
repaymentsOrWaivers.sort(LoanTransactionComparator.INSTANCE);
return repaymentsOrWaivers;
}
public List<LoanTransaction> retrieveListOfTransactionsByType(final LoanTransactionType transactionType) {
return this.loanTransactions.stream()
.filter(transaction -> transaction.isNotReversed() && transaction.getTypeOf().equals(transactionType))
@@ -880,11 +867,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.orElse(null);
}
public Money calculateTotalRecoveredPayments() {
// in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments
return getTotalRecoveredPayments();
}
public MonetaryCurrency loanCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
@@ -1033,17 +1015,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return cumulativePaid;
}
public Money getTotalRecoveredPayments() {
Money cumulativePaid = Money.zero(getCurrency());
for (final LoanTransaction recoveredPayment : this.loanTransactions) {
if (recoveredPayment.isRecoveryRepayment()) {
cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(getCurrency()));
}
}
return cumulativePaid;
}
public Money getTotalPrincipalOutstandingUntil(LocalDate date) {
return getRepaymentScheduleInstallments().stream()
.filter(installment -> installment.getDueDate().isBefore(date) || installment.getDueDate().isEqual(date))
@@ -1236,7 +1207,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.filter(date -> DateUtils.isBefore(getDisbursementDate(), date)).max(LocalDate::compareTo).orElse(getDisbursementDate());
}
private boolean isUserTransaction(LoanTransaction transaction) {
public boolean isUserTransaction(LoanTransaction transaction) {
return !(transaction.isReversed() || transaction.isAccrualRelated() || transaction.isIncomePosting());
}
@@ -1391,47 +1362,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return recalculatedOn;
}
public void updateLoanOutstandingBalances() {
Money outstanding = Money.zero(getCurrency());
List<LoanTransaction> loanTransactions = retrieveListOfTransactionsExcludeAccruals();
for (LoanTransaction loanTransaction : loanTransactions) {
if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting() || loanTransaction.isCapitalizedIncome()) {
outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency()))
.minus(loanTransaction.getOverPaymentPortion(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency());
if (loanTransaction.isOverPaid()) {
// in case of advanced payment strategy and creditAllocations the full amount is recognized first
if (this.getCreditAllocationRules() != null && !this.getCreditAllocationRules().isEmpty()) {
Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() //
.map(mapping -> mapping.getPrincipalPortion(getCurrency())) //
.reduce(Money.zero(getCurrency()), Money::plus);
transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency()).minus(payedPrincipal);
} else {
// in case legacy payment strategy
transactionOutstanding = loanTransaction.getAmount(getCurrency())
.minus(loanTransaction.getOverPaymentPortion(getCurrency()));
}
if (transactionOutstanding.isLessThanZero()) {
transactionOutstanding = Money.zero(getCurrency());
}
}
outstanding = outstanding.plus(transactionOutstanding);
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
} else if (!loanTransaction.isAccrualActivity()) {
if (this.loanInterestRecalculationDetails != null
&& this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction()
&& !loanTransaction.isRepaymentAtDisbursement()) {
outstanding = outstanding.minus(loanTransaction.getAmount(getCurrency()));
} else {
outstanding = outstanding.minus(loanTransaction.getPrincipalPortion(getCurrency()));
}
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
}
}
}
public String transactionProcessingStrategy() {
return this.transactionProcessingStrategyCode;
}
@@ -911,10 +911,6 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|| this.isIncomePosting());
}
public boolean hasLoanTransactionRelations() {
return !loanTransactionRelations.isEmpty();
}
public List<LoanTransactionRelation> getLoanTransactionRelations(Predicate<LoanTransactionRelation> predicate) {
return loanTransactionRelations.stream().filter(predicate).toList();
}
@@ -127,7 +127,8 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT
)
""")
Optional<LocalDate> findLastTransactionDateForReprocessing(@Param("loan") Loan loan);
@@ -174,7 +175,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
AND lt.typeOf IN :types
AND lt.dateOf = :transactionDate
""")
Optional<LoanTransaction> findNonReversedByLoanAndTypesAndDate(@Param("loan") Loan loan, @Param("type") Set<LoanTransactionType> types,
Optional<LoanTransaction> findNonReversedByLoanAndTypesAndDate(@Param("loan") Loan loan, @Param("types") Set<LoanTransactionType> types,
@Param("transactionDate") LocalDate transactionDate);
@Query("""
@@ -184,7 +185,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
AND lt.reversed = false
AND lt.typeOf IN :types
AND lt.dateOf > :transactionDate
ORDER BY lt.dateOf
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedByLoanAndTypesAndAfterDate(@Param("loan") Loan loan,
@Param("types") Set<LoanTransactionType> types, @Param("transactionDate") LocalDate transactionDate);
@@ -196,7 +197,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
AND lt.reversed = false
AND lt.typeOf = :type
AND lt.dateOf > :transactionDate
ORDER BY lt.dateOf
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedByLoanAndTypeAndAfterDate(@Param("loan") Loan loan, @Param("type") LoanTransactionType type,
@Param("transactionDate") LocalDate transactionDate);
@@ -219,6 +220,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
AND lt.reversed = false
AND lt.typeOf IN :types
AND lt.id NOT IN :existingTransactionIds
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedByLoanAndTypesAndNotInIds(@Param("loan") Loan loan, @Param("types") Set<LoanTransactionType> types,
@Param("existingTransactionIds") List<Long> existingTransactionIds);
@@ -229,6 +231,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf IN :types
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedByLoanAndTypes(@Param("loan") Loan loan, @Param("types") Set<LoanTransactionType> types);
@@ -238,6 +241,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf = :type
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedByLoanAndType(@Param("loan") Loan loan, @Param("type") LoanTransactionType type);
@@ -248,7 +252,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
AND lt.reversed = false
AND lt.dateOf >= :date
AND lt.typeOf IN :types
ORDER BY lt.dateOf
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedByLoanAndTypesAndOnOrAfterDate(@Param("loan") Loan loan,
@Param("types") Set<LoanTransactionType> types, @Param("date") LocalDate date);
@@ -333,7 +337,7 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
AND lt.reversed = false
AND lt.typeOf = :type
AND lt.dateOf IN :transactionDates
ORDER BY lt.id
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedLoanAndTypeAndDates(@Param("loan") Loan loan, @Param("type") LoanTransactionType type,
@Param("transactionDates") Set<LocalDate> transactionDates);
@@ -367,4 +371,90 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
AND ltr.relationType = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.ADJUSTMENT
""")
List<LoanTransaction> findAdjustmentsForCapitalizedIncome(@Param("capitalizedIncome") LoanTransaction capitalizedIncome);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf NOT IN (
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRA,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MARKED_FOR_RESCHEDULING,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.APPROVE_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT
)
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedTransactionsForReprocessingByLoan(@Param("loan") Loan loan);
@Query("""
SELECT MAX(lt.dateOf) FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.amount > 0
AND lt.typeOf IN (
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MERCHANT_ISSUED_REFUND,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.PAYOUT_REFUND,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.GOODWILL_CREDIT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_REFUND,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_ADJUSTMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.DOWN_PAYMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INTEREST_PAYMENT_WAIVER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INTEREST_REFUND,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT
)
""")
Optional<LocalDate> findLastRepaymentLikeTransactionDate(@Param("loan") Loan loan);
@Query("""
SELECT CASE WHEN COUNT(lt) > 0 THEN true ELSE false END
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf = :type
AND lt.dateOf > :transactionDate
""")
boolean existsNonReversedByLoanAndTypeAndAfterDate(@Param("loan") Loan loan, @Param("type") LoanTransactionType type,
@Param("transactionDate") LocalDate transactionDate);
@Query("""
SELECT lt FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf NOT IN (
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRA,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.MARKED_FOR_RESCHEDULING,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ADJUSTMENT,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.ACCRUAL_ACTIVITY,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.APPROVE_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.INITIATE_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REJECT_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.WITHDRAW_TRANSFER,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGE_OFF,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REAMORTIZE,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REAGE,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CONTRACT_TERMINATION,
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT
)
ORDER BY lt.dateOf, lt.createdDate, lt.id
""")
List<LoanTransaction> findNonReversedMonetaryTransactionsByLoan(@Param("loan") Loan loan);
@Query("""
SELECT COALESCE(SUM(lt.amount), 0)
FROM LoanTransaction lt
WHERE lt.loan = :loan
AND lt.reversed = false
AND lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.RECOVERY_REPAYMENT
""")
BigDecimal calculateTotalRecoveryPaymentAmount(@Param("loan") Loan loan);
}
@@ -37,7 +37,6 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
public void reprocess(final MonetaryCurrency currency, final LocalDate disbursementDate,
final List<LoanRepaymentScheduleInstallment> installments, LoanCharge loanCharge) {
Loan loan = loanCharge.getLoan();
Money zero = Money.zero(currency);
Money totalInterest = zero;
Money totalPrincipal = zero;
@@ -217,11 +217,9 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.resetDerivedComponents();
handleRefund(loanTransaction, currency, installments, charges);
} else if (loanTransaction.isCreditBalanceRefund()) {
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed,
overpaymentHolder);
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, overpaymentHolder);
} else if (loanTransaction.isChargeback()) {
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed,
overpaymentHolder);
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, overpaymentHolder);
reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed);
} else if (loanTransaction.isChargeOff()) {
recalculateChargeOffTransaction(changedTransactionDetail, loanTransaction, currency, installments);
@@ -475,7 +473,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
protected void reprocessInstallments(LocalDate disbursementDate, List<LoanTransaction> transactions,
List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency) {
LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1);
LoanRepaymentScheduleInstallment lastInstallment = installments.getLast();
if (lastInstallment.isAdditional() && lastInstallment.getDue(currency).isZero()) {
installments.remove(lastInstallment);
}
@@ -519,8 +517,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
private void recalculateCreditTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, List<LoanTransaction> transactionsToBeProcessed,
MoneyHolder overpaymentHolder) {
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaymentHolder) {
// pass through for new transactions
if (loanTransaction.getId() == null) {
return;
@@ -606,7 +603,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
// New installment will be added (N+1 scenario)
if (!loanTransactionMapped) {
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed.get(installmentToBeProcessed.size() - 1);
LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed.getLast();
currentInstallment.addToCreditedPrincipal(transactionAmount.getAmount());
currentInstallment.addToPrincipal(transactionDate, transactionAmount);
if (repaidAmount.isGreaterThanZero()) {
@@ -647,7 +644,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
Integer installmentNumber = null;
if (loanTransaction.isChargePayment() && installments.size() == 1) {
installmentNumber = installments.get(0).getInstallmentNumber();
installmentNumber = installments.getFirst().getInstallmentNumber();
}
if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual() && !loanTransaction.isAccrualActivity()) {
@@ -944,12 +941,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return null;
}
LoanCharge latestCharge = null;
List<LoanCharge> chargesWithSpecificDueDate = new ArrayList<>();
chargesWithSpecificDueDate.addAll(charges.stream().filter(charge -> charge.isSpecifiedDueDate()).toList());
final List<LoanCharge> chargesWithSpecificDueDate = new ArrayList<>(
charges.stream().filter(LoanCharge::isSpecifiedDueDate).toList());
if (!CollectionUtils.isEmpty(chargesWithSpecificDueDate)) {
Collections.sort(chargesWithSpecificDueDate,
(charge1, charge2) -> DateUtils.compare(charge1.getEffectiveDueDate(), charge2.getEffectiveDueDate()));
latestCharge = chargesWithSpecificDueDate.get(chargesWithSpecificDueDate.size() - 1);
chargesWithSpecificDueDate
.sort((charge1, charge2) -> DateUtils.compare(charge1.getEffectiveDueDate(), charge2.getEffectiveDueDate()));
latestCharge = chargesWithSpecificDueDate.getLast();
}
return latestCharge;
}
@@ -56,6 +56,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod;
@@ -63,13 +64,12 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleP
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.ScheduleDateException;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
@RequiredArgsConstructor
public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanScheduleGenerator {
private final LoanTransactionService loanTransactionService;
private final LoanTransactionRepository loanTransactionRepository;
private final CurrencyMapper currencyMapper;
@Override
@@ -404,7 +404,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
// this condition is to add the interest from grace period if not
// already applied.
if (scheduleParams.getTotalOutstandingInterestPaymentDueToGrace().isGreaterThanZero()) {
LoanScheduleModelPeriod installment = periods.get(periods.size() - 1);
LoanScheduleModelPeriod installment = periods.getLast();
installment.addInterestAmount(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace());
scheduleParams.addTotalRepaymentExpected(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace());
scheduleParams.addTotalCumulativeInterest(scheduleParams.getTotalOutstandingInterestPaymentDueToGrace());
@@ -421,7 +421,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
if (scheduleParams.getScheduleTillDate() != null) {
currentDate = scheduleParams.getScheduleTillDate();
}
if (scheduleParams.applyInterestRecalculation() && scheduleParams.getLatePaymentMap().size() > 0
if (scheduleParams.applyInterestRecalculation() && !scheduleParams.getLatePaymentMap().isEmpty()
&& DateUtils.isAfter(currentDate, scheduleParams.getPeriodStartDate())) {
Money totalInterest = addInterestOnlyRepaymentScheduleForCurrentDate(mc, loanApplicationTerms, holidayDetailDTO,
monetaryCurrency, periods, currentDate, loanRepaymentScheduleTransactionProcessor, transactions, loanCharges,
@@ -1345,9 +1345,8 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
return principalToBeScheduled.minus(loanApplicationTerms.getDownPaymentAmount());
}
private boolean updateFixedInstallmentAmount(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, int periodNumber,
private void updateFixedInstallmentAmount(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, int periodNumber,
Money outstandingBalance) {
boolean isAmountChanged = false;
if (loanApplicationTerms.getActualFixedEmiAmount() == null && loanApplicationTerms.getInterestMethod().isDecliningBalance()
&& loanApplicationTerms.getAmortizationMethod().isEqualInstallment()) {
if (periodNumber < loanApplicationTerms.getPrincipalGrace() + 1) {
@@ -1356,9 +1355,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
Money emiAmount = loanApplicationTerms.pmtForInstallment(getPaymentPeriodsInOneYearCalculator(), outstandingBalance,
periodNumber, mc);
loanApplicationTerms.setFixedEmiAmount(emiAmount.getAmount());
isAmountChanged = true;
}
return isAmountChanged;
}
private Money fetchArrears(final LoanApplicationTerms loanApplicationTerms, final MonetaryCurrency currency,
@@ -1984,7 +1981,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
// this method relates to multi-disbursement loans
BigDecimal principal = BigDecimal.ZERO;
if (loanApplicationTerms.getDisbursementDatas().size() == 0) {
if (loanApplicationTerms.getDisbursementDatas().isEmpty()) {
// non tranche loans have no disbursement data entries in submitted and approved status
// the appropriate approved amount or applied for amount is used to show a proposed schedule
if (loanApplicationTerms.getApprovedPrincipal().getAmount().compareTo(BigDecimal.ZERO) > 0) {
@@ -2521,10 +2518,9 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
}
if (retainedInstallments.size() > 0
&& retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion() != null) {
if (!retainedInstallments.isEmpty() && retainedInstallments.getLast().getRescheduleInterestPortion() != null) {
loanApplicationTerms.setInterestTobeApproppriated(
Money.of(loan.getCurrency(), retainedInstallments.get(retainedInstallments.size() - 1).getRescheduleInterestPortion()));
Money.of(loan.getCurrency(), retainedInstallments.getLast().getRescheduleInterestPortion()));
}
LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO,
loanScheduleParams);
@@ -2723,19 +2719,18 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
return principalPortionCalculated;
}
private LoanRepaymentScheduleInstallment addLoanRepaymentScheduleInstallment(final List<LoanRepaymentScheduleInstallment> installments,
private void addLoanRepaymentScheduleInstallment(final List<LoanRepaymentScheduleInstallment> installments,
final LoanScheduleModelPeriod scheduledLoanInstallment) {
LoanRepaymentScheduleInstallment installment = null;
if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) {
installment = new LoanRepaymentScheduleInstallment(null, scheduledLoanInstallment.periodNumber(),
scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(),
scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.interestDue(),
scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.penaltyChargesDue(),
scheduledLoanInstallment.isRecalculatedInterestComponent(), scheduledLoanInstallment.getLoanCompoundingDetails(),
scheduledLoanInstallment.rescheduleInterestPortion(), scheduledLoanInstallment.isDownPaymentPeriod());
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(null,
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent(),
scheduledLoanInstallment.getLoanCompoundingDetails(), scheduledLoanInstallment.rescheduleInterestPortion(),
scheduledLoanInstallment.isDownPaymentPeriod());
installments.add(installment);
}
return installment;
}
private LoanScheduleModelPeriod createLoanScheduleModelDownPaymentPeriod(final LoanRepaymentScheduleInstallment installment,
@@ -2804,7 +2799,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
LoanScheduleDTO loanScheduleDTO = rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO,
loanRepaymentScheduleTransactionProcessor, onDate, calculateTill);
final List<LoanTransaction> loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(),
loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges());
@@ -29,7 +29,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.springframework.stereotype.Component;
@@ -63,9 +63,9 @@ public class CumulativeDecliningBalanceInterestLoanScheduleGenerator extends Abs
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
public CumulativeDecliningBalanceInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator,
final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator, final LoanTransactionService loanTransactionService,
final CurrencyMapper currencyMapper) {
super(loanTransactionService, currencyMapper);
final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator,
final LoanTransactionRepository loanTransactionRepository, final CurrencyMapper currencyMapper) {
super(loanTransactionRepository, currencyMapper);
this.scheduledDateGenerator = scheduledDateGenerator;
this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator;
}
@@ -27,7 +27,7 @@ import java.util.TreeMap;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.springframework.stereotype.Component;
@Component
@@ -37,9 +37,9 @@ public class CumulativeFlatInterestLoanScheduleGenerator extends AbstractCumulat
private final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator;
public CumulativeFlatInterestLoanScheduleGenerator(final ScheduledDateGenerator scheduledDateGenerator,
final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator, final LoanTransactionService loanTransactionService,
final CurrencyMapper currencyMapper) {
super(loanTransactionService, currencyMapper);
final PaymentPeriodsInOneYearCalculator paymentPeriodsInOneYearCalculator,
final LoanTransactionRepository loanTransactionRepository, final CurrencyMapper currencyMapper) {
super(loanTransactionRepository, currencyMapper);
this.scheduledDateGenerator = scheduledDateGenerator;
this.paymentPeriodsInOneYearCalculator = paymentPeriodsInOneYearCalculator;
}
@@ -19,18 +19,24 @@
package org.apache.fineract.portfolio.loanaccount.serialization;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public final class LoanChargeValidator {
private final LoanTransactionRepository loanTransactionRepository;
public void validateLoanIsNotClosed(final Loan loan, final LoanCharge loanCharge) {
if (loan.isClosed()) {
final String defaultUserMessage = "This charge cannot be added as the loan is already closed.";
@@ -80,13 +86,13 @@ public final class LoanChargeValidator {
public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransaction repaymentTransaction,
final String reversedOrCreated) {
if (repaymentTransaction.isRepaymentLikeType() && !repaymentTransaction.isChargeRefund()) {
for (LoanTransaction txn : loan.getLoanTransactions()) {
if (txn.isChargeRefund() && DateUtils.isBefore(repaymentTransaction.getTransactionDate(), txn.getTransactionDate())) {
final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists";
final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated
+ " because a Later Charge Refund Exists.";
throw new LoanChargeRefundException(errorMessage, details);
}
final boolean existsChargeRefund = loanTransactionRepository.existsNonReversedByLoanAndTypeAndAfterDate(loan,
LoanTransactionType.CHARGE_REFUND, repaymentTransaction.getTransactionDate());
if (existsChargeRefund) {
final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists";
final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated
+ " because a Later Charge Refund Exists.";
throw new LoanChargeRefundException(errorMessage, details);
}
}
}
@@ -21,10 +21,12 @@ package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.persistence.FlushModeType;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -34,6 +36,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
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.loanproduct.domain.CreditAllocationTransactionType;
import org.springframework.stereotype.Service;
@@ -43,6 +47,7 @@ public class LoanBalanceService {
private final CapitalizedIncomeBalanceService capitalizedIncomeBalanceService;
private final FlushModeHandler flushModeHandler;
private final LoanTransactionRepository loanTransactionRepository;
public Money calculateTotalOverpayment(final Loan loan) {
Money totalPaidInRepayments = loan.getTotalPaidInRepayments();
@@ -107,7 +112,7 @@ public class LoanBalanceService {
loan.setTotalOverpaid(overpaidBy.getAmountDefaultedToNullIfZero());
}
final Money recoveredAmount = loan.calculateTotalRecoveredPayments();
final Money recoveredAmount = calculateTotalRecoveredPayments(loan);
loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero());
final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal();
@@ -115,7 +120,61 @@ public class LoanBalanceService {
final Money capitalizedIncomeAdjustment = capitalizedIncomeBalanceService.calculateCapitalizedIncomeAdjustment(loan);
loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges(),
capitalizedIncome, capitalizedIncomeAdjustment);
loan.updateLoanOutstandingBalances();
updateLoanOutstandingBalances(loan);
}
private Money calculateTotalRecoveredPayments(Loan loan) {
// in case logic for reversing recovered payment is implemented handle subtraction from totalRecoveredPayments
final BigDecimal totalRecoveryAmount = loanTransactionRepository.calculateTotalRecoveryPaymentAmount(loan);
return Money.of(loan.getCurrency(), totalRecoveryAmount);
}
public void updateLoanOutstandingBalances(Loan loan) {
Money outstanding = Money.zero(loan.getCurrency());
final List<LoanTransaction> loanTransactions = new ArrayList<>();
for (final LoanTransaction transaction : loan.getLoanTransactions()) {
if (transaction.isNotReversed() && !transaction.isNonMonetaryTransaction()) {
loanTransactions.add(transaction);
}
}
loanTransactions.sort(LoanTransactionComparator.INSTANCE);
for (LoanTransaction loanTransaction : loanTransactions) {
if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting() || loanTransaction.isCapitalizedIncome()) {
outstanding = outstanding.plus(loanTransaction.getAmount(loan.getCurrency()))
.minus(loanTransaction.getOverPaymentPortion(loan.getCurrency()));
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
} else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) {
Money transactionOutstanding = loanTransaction.getPrincipalPortion(loan.getCurrency());
if (loanTransaction.isOverPaid()) {
// in case of advanced payment strategy and creditAllocations the full amount is recognized first
if (loan.getCreditAllocationRules() != null && !loan.getCreditAllocationRules().isEmpty()) {
Money payedPrincipal = loanTransaction.getLoanTransactionToRepaymentScheduleMappings().stream() //
.map(mapping -> mapping.getPrincipalPortion(loan.getCurrency())) //
.reduce(Money.zero(loan.getCurrency()), Money::plus);
transactionOutstanding = loanTransaction.getPrincipalPortion(loan.getCurrency()).minus(payedPrincipal);
} else {
// in case legacy payment strategy
transactionOutstanding = loanTransaction.getAmount(loan.getCurrency())
.minus(loanTransaction.getOverPaymentPortion(loan.getCurrency()));
}
if (transactionOutstanding.isLessThanZero()) {
transactionOutstanding = Money.zero(loan.getCurrency());
}
}
outstanding = outstanding.plus(transactionOutstanding);
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
} else if (!loanTransaction.isAccrualActivity()) {
if (loan.getLoanInterestRecalculationDetails() != null
&& loan.getLoanInterestRecalculationDetails().isCompoundingToBePostedAsTransaction()
&& !loanTransaction.isRepaymentAtDisbursement()) {
outstanding = outstanding.minus(loanTransaction.getAmount(loan.getCurrency()));
} else {
outstanding = outstanding.minus(loanTransaction.getPrincipalPortion(loan.getCurrency()));
}
loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount()));
}
}
}
public void updateLoanToLastDisbursalState(final Loan loan, final LoanDisbursementDetails disbursementDetail) {
@@ -84,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.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
@@ -93,7 +94,6 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
@@ -121,17 +121,17 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final InterestRefundService interestRefundService;
private final LoanScheduleComponent loanSchedule;
private final LoanTransactionService loanTransactionService;
private final LoanTransactionRepository loanTransactionRepository;
public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper,
final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory,
final LoanScheduleComponent loanSchedule, final LoanTransactionService loanTransactionRepository,
final LoanScheduleComponent loanSchedule, final LoanTransactionRepository loanTransactionRepository,
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
super(externalIdFactory, loanChargeValidator, loanBalanceService);
this.emiCalculator = emiCalculator;
this.loanRepositoryWrapper = loanRepositoryWrapper;
this.interestRefundService = interestRefundService;
this.loanTransactionService = loanTransactionRepository;
this.loanTransactionRepository = loanTransactionRepository;
this.loanSchedule = loanSchedule;
}
@@ -212,7 +212,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency));
final Loan loan = loanTransactions.get(0).getLoan();
final Loan loan = loanTransactions.getFirst().getLoan();
List<LoanTermVariationsData> loanTermVariations = loan.getActiveLoanTermVariations().stream().map(LoanTermVariations::toData)
.collect(Collectors.toCollection(ArrayList::new));
final Integer installmentAmountInMultiplesOf = loan.getLoanProductRelatedDetail().getInstallmentAmountInMultiplesOf();
@@ -276,7 +276,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) {
Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
final List<LoanTransaction> transactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> transactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency();
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
Set<LoanCharge> charges = loan.getActiveCharges();
@@ -552,8 +552,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
if (!loanTransactionMapped) {
if (transactionDate.equals(pastDueDate)) {
// Transaction is on Maturity date, no additional installment is needed
LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed
.get(installmentToBeProcessed.size() - 1);
LoanRepaymentScheduleInstallment currentInstallment = installmentToBeProcessed.getLast();
emiCalculator.creditPrincipal(model, transactionDate, transactionAmount);
updateRepaymentPeriods(loanTransaction, progressiveTransactionCtx);
@@ -827,7 +826,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// New installment will be added (N+1 scenario)
if (!loanTransactionMapped) {
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments().get(ctx.getInstallments().size() - 1);
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments().getLast();
recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation);
} else {
Loan loan = loanTransaction.getLoan();
@@ -1450,7 +1449,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) {
Loan loan = ctx.getInstallments().get(0).getLoan();
Loan loan = ctx.getInstallments().getFirst().getLoan();
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isNpa() && !ctx.isChargedOff()
&& !ctx.isContractTerminated()) {
@@ -1761,8 +1760,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
.max(LocalDate::compareTo);
if (latestDueDate.isPresent()) {
final LoanRepaymentScheduleInstallment lastInstallment = installmentsUpToTransactionDate
.get(installmentsUpToTransactionDate.size() - 1);
final LoanRepaymentScheduleInstallment lastInstallment = installmentsUpToTransactionDate.getLast();
final LoanRepaymentScheduleInstallment installmentForCharges = new LoanRepaymentScheduleInstallment(loan,
lastInstallment.getInstallmentNumber() + 1, currentInstallment.getDueDate(), latestDueDate.get(),
@@ -148,7 +148,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
LoanTransaction savedCapitalizedIncomeAdjustment = loanTransactionRepository.saveAndFlush(capitalizedIncomeAdjustment);
// Update outstanding loan balances
loan.updateLoanOutstandingBalances();
loanBalanceService.updateLoanOutstandingBalances(loan);
// Create a note if provided
final String noteText = command.stringValueOfParameterNamed("note");
@@ -40,6 +40,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.springframework.beans.factory.InitializingBean;
@@ -56,9 +57,8 @@ public class InternalProgressiveLoanApiResource implements InitializingBean {
private final LoanRepositoryWrapper loanRepository;
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
private final ProgressiveLoanInterestScheduleModelParserService progressiveLoanInterestScheduleModelParserService;
private final InterestScheduleModelRepositoryWrapper writePlatformService;
private final LoanTransactionService loanTransactionService;
private final LoanTransactionRepository loanTransactionRepository;
@Override
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
@@ -87,7 +87,8 @@ public class InternalProgressiveLoanApiResource implements InitializingBean {
}
private ProgressiveLoanInterestScheduleModel reprocessTransactionsAndGetModel(final Loan loan) {
final List<LoanTransaction> transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> transactionsToReprocess = loanTransactionRepository
.findNonReversedTransactionsForReprocessingByLoan(loan);
final LocalDate businessDate = ThreadLocalContextUtil.getBusinessDate();
final Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
.reprocessProgressiveLoanTransactionsTransactional(loan.getDisbursementDate(), businessDate, transactionsToReprocess,
@@ -67,6 +67,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
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.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
@@ -74,7 +75,6 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -120,7 +120,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(LoanRepositoryWrapper.class),
Mockito.mock(InterestRefundService.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class),
Mockito.mock(LoanTransactionService.class), Mockito.mock(LoanChargeValidator.class),
Mockito.mock(LoanTransactionRepository.class), Mockito.mock(LoanChargeValidator.class),
Mockito.mock(LoanBalanceService.class));
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
@@ -134,7 +134,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final ConfigurationDomainService configurationDomainService;
private final HolidayRepository holidayRepository;
private final WorkingDaysRepositoryWrapper workingDaysRepository;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final NoteRepository noteRepository;
private final BusinessEventNotifierService businessEventNotifierService;
@@ -464,7 +463,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
chargesPayment.updateComponents(zero, zero, charge.getAmount(loan.getCurrency()), zero);
chargesPayment.updateLoan(loan);
loan.addLoanTransaction(chargesPayment);
loan.updateLoanOutstandingBalances();
loanBalanceService.updateLoanOutstandingBalances(loan);
charge.markAsFullyPaid();
}
@@ -931,7 +931,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
createUpdateIncomePostingTransaction(loan, compoundingDetail, interest, fee, penalties, externalId);
createUpdateAccrualTransaction(loan, compoundingDetail, interest, fee, penalties, feeDetails, externalId);
loan.updateLoanOutstandingBalances();
loanBalanceService.updateLoanOutstandingBalances(loan);
}
private void createUpdateIncomePostingTransaction(Loan loan, LoanInterestRecalcualtionAdditionalDetails compoundingDetail,
@@ -1000,7 +1000,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
createIncomePostingAndAccrualTransactionOnLoanClosure(loan, closedDate, interestToPost, feeToPost, penaltyToPost, amountToPost);
}
loan.updateLoanOutstandingBalances();
loanBalanceService.updateLoanOutstandingBalances(loan);
}
private void determineCumulativeIncomeFromInstallments(final Loan loan,
@@ -261,20 +261,20 @@ public class LoanAssemblerImpl implements LoanAssembler {
loanOfficer, loanPurpose, transactionProcessingStrategy, loanProductRelatedDetail, loanCharges,
syncDisbursementWithMeeting, fixedEmiAmount, disbursementDetails, maxOutstandingLoanBalance,
createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential, rates,
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel,
isEnableInstallmentLevelDelinquency, submittedOnDate);
fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, isEnableInstallmentLevelDelinquency,
submittedOnDate);
} else if (group != null) {
loanApplication = Loan.newGroupLoanApplication(accountNo, group, loanAccountType, loanProduct, fund, loanOfficer, loanPurpose,
transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, syncDisbursementWithMeeting, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms,
loanScheduleModel, isEnableInstallmentLevelDelinquency, submittedOnDate);
isEnableInstallmentLevelDelinquency, submittedOnDate);
} else if (client != null) {
loanApplication = Loan.newIndividualLoanApplication(accountNo, client, loanAccountType, loanProduct, fund, loanOfficer,
loanPurpose, transactionProcessingStrategy, loanProductRelatedDetail, loanCharges, collateral, fixedEmiAmount,
disbursementDetails, maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate,
interestRateDifferential, rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms,
loanScheduleModel, isEnableInstallmentLevelDelinquency, submittedOnDate);
isEnableInstallmentLevelDelinquency, submittedOnDate);
} else {
throw new IllegalStateException("No loan application exists for either a client or group (or both).");
}
@@ -63,6 +63,7 @@ public class LoanDisbursementService {
private final LoanDisbursementValidator loanDisbursementValidator;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanChargeService loanChargeService;
private final LoanBalanceService loanBalanceService;
public void updateDisbursementDetails(final Loan loan, final JsonCommand jsonCommand, final Map<String, Object> actualChanges) {
final List<Long> disbursementList = loan.fetchDisbursementIds();
@@ -217,7 +218,7 @@ public class LoanDisbursementService {
chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero);
chargesPayment.updateLoan(loan);
loan.addLoanTransaction(chargesPayment);
loan.updateLoanOutstandingBalances();
loanBalanceService.updateLoanOutstandingBalances(loan);
}
final LocalDate expectedDate = loan.getExpectedFirstRepaymentOnDate();
@@ -61,8 +61,8 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final LoanTermVariationsMapper loanMapper;
private final InterestScheduleModelRepositoryWrapper modelRepository;
private final LoanTransactionService loanTransactionService;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionService loanTransactionService;
@Override
public boolean canProcessLatestTransactionOnly(Loan loan, LoanTransaction loanTransaction,
@@ -594,8 +594,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
if (loan.getLoanProduct().isMultiDisburseLoan() || loan.isProgressiveSchedule()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loanTransactionService
.retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loanTransactionRepository
.findNonReversedTransactionsForReprocessingByLoan(loan);
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
@@ -2246,7 +2246,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
} else {
if (disbursementDataArray == null || disbursementDataArray.size() == 0) {
if (disbursementDataArray == null || disbursementDataArray.isEmpty()) {
final String errorMessage = "For this loan product, disbursement details must be provided";
throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage);
}
@@ -2821,7 +2821,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
}
final List<LoanTransaction> loanTransactions = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> loanTransactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
loanTransactions.add(chargeOffTransaction);
reprocessLoanTransactionsService.reprocessParticularTransactions(loan, loanTransactions);
loan.addLoanTransaction(chargeOffTransaction);
@@ -3031,7 +3031,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal";
throw new LoanMultiDisbursementException(errorMessage);
}
Integer trancheDisbursedCount = 0;
int trancheDisbursedCount = 0;
for (LoanDisbursementDetails disbursementDetails : loan.getDisbursementDetails()) {
if (disbursementDetails.actualDisbursementDate() != null) {
trancheDisbursedCount++;
@@ -3361,13 +3361,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Map<String, Object> actualChanges = new LinkedHashMap<>();
List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsByType(LoanTransactionType.DISBURSEMENT);
loanTransactions.sort(Comparator.comparing(LoanTransaction::getId));
final LoanTransaction lastDisbursalTransaction = loanTransactions.get(loanTransactions.size() - 1);
final LoanTransaction lastDisbursalTransaction = loanTransactions.getLast();
final LocalDate lastTransactionDate = lastDisbursalTransaction.getTransactionDate();
existingTransactionIds.addAll(loanTransactionRepository.findTransactionIdsByLoan(loan));
existingReversedTransactionIds.addAll(loanTransactionRepository.findReversedTransactionIdsByLoan(loan));
loanTransactions = loan.retrieveListOfTransactionsExcludeAccruals();
loanTransactions = loanTransactionRepository.findNonReversedMonetaryTransactionsByLoan(loan);
Collections.reverse(loanTransactions);
for (final LoanTransaction previousTransaction : loanTransactions) {
if (DateUtils.isBefore(lastTransactionDate, previousTransaction.getTransactionDate())
@@ -36,6 +36,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
@@ -53,7 +54,7 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
private final EMICalculator emiCalculator;
private final LoanRepositoryWrapper loanRepository;
private final LoanTransactionService loanTransactionService;
private final LoanTransactionRepository loanTransactionRepository;
private final InterestScheduleModelRepositoryWrapper modelRepository;
@Override
@@ -81,8 +82,8 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
}
private ProgressiveLoanInterestScheduleModel calculateModel(Loan loan, LocalDate businessDate) {
final List<LoanTransaction> transactionsToReprocess = loanTransactionService.retrieveListOfTransactionsForReprocessing(loan)
.stream().filter(t -> !t.isAccrualActivity()).toList();
final List<LoanTransaction> transactionsToReprocess = loanTransactionRepository
.findNonReversedTransactionsForReprocessingByLoan(loan).stream().filter(t -> !t.isAccrualActivity()).toList();
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> changedTransactionDetailProgressiveLoanInterestScheduleModelPair = advancedPaymentScheduleTransactionProcessor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), businessDate, transactionsToReprocess, loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
@@ -22,8 +22,6 @@ import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -37,7 +35,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
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.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
@@ -55,26 +52,18 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepositoryWrapper;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionRepository loanTransactionRepository;
private final LoanTransactionService loanTransactionService;
@Override
public void reprocessTransactions(final Loan loan) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loanTransactionService
.retrieveListOfTransactionsForReprocessing(loan);
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan,
allNonContraTransactionsPostDisbursement);
handleChangedDetail(changedTransactionDetail);
}
private List<LoanTransaction> retrieveListOfTransactionsForReprocessing(final Loan loan) {
return loan.getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate())
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
}
private Predicate<LoanTransaction> loanTransactionForReprocessingPredicate() {
return transaction -> transaction.isNotReversed()
&& (transaction.isChargeOff() || transaction.isReAge() || transaction.isAccrualActivity() || transaction.isReAmortize()
|| !transaction.isNonMonetaryTransaction() || transaction.isContractTermination());
}
@Override
public void reprocessParticularTransactions(final Loan loan, final List<LoanTransaction> loanTransactions) {
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, loanTransactions);
@@ -83,7 +72,7 @@ public class ReprocessLoanTransactionsServiceImpl implements ReprocessLoanTransa
@Override
public void reprocessTransactionsWithPostTransactionChecks(final Loan loan, final LocalDate transactionDate) {
final List<LoanTransaction> transactions = retrieveListOfTransactionsForReprocessing(loan);
final List<LoanTransaction> transactions = loanTransactionRepository.findNonReversedTransactionsForReprocessingByLoan(loan);
final ChangedTransactionDetail changedTransactionDetail = reprocessTransactionsAndFetchChangedTransactions(loan, transactions);
handleChangedDetail(changedTransactionDetail);
}
@@ -70,10 +70,10 @@ public class LoanContractTerminationServiceImpl {
private final LoanUtilService loanUtilService;
private final ExternalIdFactory externalIdFactory;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionService loanTransactionService;
private final LoanScheduleService loanScheduleService;
private final LoanChargeValidator loanChargeValidator;
private final ProgressiveLoanTransactionValidator loanTransactionValidator;
private final LoanTransactionService loanTransactionService;
public CommandProcessingResult applyContractTermination(final JsonCommand command) {
final Loan loan = loanAssembler.assembleFrom(command.getLoanId());
@@ -22,6 +22,7 @@ import java.util.List;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
@@ -35,7 +36,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestRefundServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
@@ -138,9 +138,9 @@ public class LoanAccountAutoStarter {
final LoanRepositoryWrapper loanRepositoryWrapper,
final @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService,
final ExternalIdFactory externalIdFactory, final LoanScheduleComponent loanSchedule,
final LoanTransactionService loanTransactionService, final LoanChargeValidator loanChargeValidator,
final LoanTransactionRepository loanTransactionRepository, final LoanChargeValidator loanChargeValidator,
final LoanBalanceService loanBalanceService) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService,
externalIdFactory, loanSchedule, loanTransactionService, loanChargeValidator, loanBalanceService);
externalIdFactory, loanSchedule, loanTransactionRepository, loanChargeValidator, loanBalanceService);
}
}
@@ -457,9 +457,9 @@ public class LoanAccountConfiguration {
@ConditionalOnMissingBean(LoanDisbursementService.class)
public LoanDisbursementService loanDisbursementService(LoanChargeValidator loanChargeValidator,
LoanDisbursementValidator loanDisbursementValidator, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanChargeService loanChargeService) {
LoanChargeService loanChargeService, LoanBalanceService loanBalanceService) {
return new LoanDisbursementService(loanChargeValidator, loanDisbursementValidator, reprocessLoanTransactionsService,
loanChargeService);
loanChargeService, loanBalanceService);
}
@Bean
@@ -363,7 +363,7 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
DelinquencyReadPlatformService delinquencyReadPlatformService = new DelinquencyReadPlatformServiceImpl(repositoryRange,
repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange, mapperBucket, mapperLoanDelinquencyTagHistory,
loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository,
delinquencyEffectivePauseHelper, configurationDomainService);
delinquencyEffectivePauseHelper, configurationDomainService, Mockito.mock(LoanTransactionRepository.class));
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.isMultiDisburseLoan()).thenReturn(false);
@@ -40,7 +40,6 @@ import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
@@ -80,7 +79,6 @@ public class LoanBuilder {
private BigDecimal fixedPrincipalPercentagePerInstallment;
private ExternalId externalId = ExternalId.empty();
private LoanApplicationTerms loanApplicationTerms = mock(LoanApplicationTerms.class);
private LoanScheduleModel loanScheduleModel = mock(LoanScheduleModel.class);
private Boolean enableInstallmentLevelDelinquency = false;
private LocalDate submittedOnDate = LocalDate.now(ZoneId.systemDefault());
private LocalDate approvedOnDate;
@@ -116,8 +114,8 @@ public class LoanBuilder {
Loan loan = Loan.newIndividualLoanApplication(accountNo, client, loanType, loanProduct, fund, loanOfficer, loanPurpose,
transactionProcessor, loanRepaymentScheduleDetail, charges, collateral, fixedEmiAmount, disbursementDetails,
maxOutstandingLoanBalance, createStandingInstructionAtDisbursement, isFloatingInterestRate, interestRateDifferential,
rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, loanScheduleModel,
enableInstallmentLevelDelinquency, submittedOnDate);
rates, fixedPrincipalPercentagePerInstallment, externalId, loanApplicationTerms, enableInstallmentLevelDelinquency,
submittedOnDate);
if (id != null) {
loan.setId(id);
@@ -335,11 +333,6 @@ public class LoanBuilder {
return this;
}
public LoanBuilder withLoanScheduleModel(LoanScheduleModel loanScheduleModel) {
this.loanScheduleModel = loanScheduleModel;
return this;
}
public LoanBuilder withEnableInstallmentLevelDelinquency(Boolean enableInstallmentLevelDelinquency) {
this.enableInstallmentLevelDelinquency = enableInstallmentLevelDelinquency;
return this;