FINERACT-2107: Interest Refund - Transaction Amount Recalculation

This commit is contained in:
Soma Sörös
2024-11-14 09:59:08 +01:00
committed by Adam Saghy
parent 4e8fc2c0a8
commit 6286d1f821
14 changed files with 276 additions and 75 deletions
@@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
@@ -143,8 +144,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
@Transactional
@Override
@@ -165,17 +166,26 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.loanCollateralManagementRepository.saveAll(loanCollateralManagementSet);
}
private LoanTransaction createInterestRefundLoanTransaction(Loan loan, final LocalDate transactionDate,
BigDecimal relatedRefundTransactionAmount) {
private LoanTransaction createInterestRefundLoanTransaction(Loan loan, LoanTransaction refundTransaction) {
InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan);
if (interestRefundService == null) {
return null;
}
BigDecimal interestRefundAmount = interestRefundService.calculateInterestRefundAmount(loan.getId(), relatedRefundTransactionAmount,
transactionDate);
Money totalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), refundTransaction.getTransactionDate(),
List.of(), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList());
Money previouslyRefundedInterests = interestRefundService.getTotalInterestRefunded(loan.getLoanTransactions(), loan.getCurrency());
Money newTotalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(),
refundTransaction.getTransactionDate(), List.of(refundTransaction),
loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList());
BigDecimal interestRefundAmount = totalInterest.minus(previouslyRefundedInterests).minus(newTotalInterest).getAmount();
final ExternalId txnExternalId = externalIdFactory.create();
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestRefundPreBusinessEvent(loan));
return LoanTransaction.interestRefund(loan, interestRefundAmount, transactionDate, txnExternalId);
return LoanTransaction.interestRefund(loan, interestRefundAmount, refundTransaction.getDateOf(), txnExternalId);
}
@Transactional
@@ -867,7 +877,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
LoanTransaction interestRefundTransaction = null;
if (shouldCreateInterestRefundTransaction) {
interestRefundTransaction = createInterestRefundLoanTransaction(loan, transactionDate, transactionAmount);
interestRefundTransaction = createInterestRefundLoanTransaction(loan, refundTransaction);
if (interestRefundTransaction != null) {
interestRefundTransaction.getLoanTransactionRelations().add(LoanTransactionRelation
.linkToTransaction(interestRefundTransaction, refundTransaction, LoanTransactionRelationTypeEnum.RELATED));
@@ -0,0 +1,37 @@
/**
* 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.service;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class InterestRefundServiceDelegate {
@Lazy
private final List<InterestRefundService> interestRefundService;
public InterestRefundService lookupInterestRefundService(final Loan loan) {
return interestRefundService.stream().filter(iRS -> iRS.canHandle(loan)).findFirst().orElse(null);
}
}
@@ -25,19 +25,22 @@ import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
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.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.starter.AdvancedPaymentScheduleTransactionProcessorCondition;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
@@ -49,18 +52,13 @@ import org.springframework.transaction.annotation.Transactional;
@Service
public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundService {
private final AdvancedPaymentScheduleTransactionProcessor processor;
private final EMICalculator emiCalculator;
private final LoanAssembler loanAssembler;
@Override
public boolean canHandle(Loan loan) {
return loan != null && loan.isInterestBearing() && processor.accept(loan.getTransactionProcessingStrategyCode());
}
private static void simulateRepaymentForDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal,
List<LoanTransaction> collect) {
collect.add(lt);
collect.add(new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), lt.getTypeOf().getValue(), lt.getDateOf(), lt.getAmount(),
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, null));
if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) {
if (lt.getAmount().compareTo(refundFinal.get()) <= 0) {
collect.add(
@@ -76,52 +74,70 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
}
}
private BigDecimal totalInterest(final Loan loan, BigDecimal refundAmount, LocalDate relatedRefundTransactionDate) {
final AtomicReference<BigDecimal> refundFinal = new AtomicReference<>(refundAmount);
private Money recalculateTotalInterest(AdvancedPaymentScheduleTransactionProcessor processor, Loan loan,
LocalDate relatedRefundTransactionDate, List<LoanTransaction> transactionsToReprocess) {
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>(
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
BigDecimal payableInterest = BigDecimal.ZERO;
if (loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isDisbursement)) {
List<LoanTransaction> transactionsToReprocess = new ArrayList<>();
List<LoanTransactionType> interestRefundTypes = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream()
.map(LoanSupportedInterestRefundTypes::getTransactionType).toList();
// add already interest refunded amounts to refund amount
// it is necessary to avoid multi disbursed refund
loan.getLoanTransactions().stream() //
.filter(lt -> !lt.isReversed()) //
.filter(lt -> interestRefundTypes.contains(lt.getTypeOf())) //
.forEach(t -> refundFinal.set(refundFinal.get().add(t.getAmount()))); //
loan.getLoanTransactions().stream() //
.filter(lt -> !lt.isReversed()) //
.filter(lt -> !lt.isAccrual() && !lt.isAccrualActivity() && !lt.isInterestRefund()) //
.filter(loanTransaction -> !interestRefundTypes.contains(loanTransaction.getTypeOf())) //
.forEach(lt -> simulateRepaymentForDisbursements(lt, refundFinal, transactionsToReprocess)); //
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet());
ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight();
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>(
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
return emiCalculator.getSumOfDueInterestsOnDate(modelAfter, relatedRefundTransactionDate);
}
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet());
ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight();
@Override
public boolean canHandle(Loan loan) {
String s = loan.getTransactionProcessingStrategyCode();
return AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME.equalsIgnoreCase(s)
|| AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equalsIgnoreCase(s);
}
payableInterest = installmentsToReprocess.stream() //
.map(installment -> emiCalculator //
.getDueAmounts(modelAfter, installment.getDueDate(), relatedRefundTransactionDate) //
.getDueInterest() //
.getAmount()) //
.reduce(BigDecimal.ZERO, BigDecimal::add); //
}
return payableInterest;
private boolean isTransactionNeededForInterestRefundCalculations(LoanTransaction lt) {
return lt.isNotReversed() && !lt.isAccrual() && !lt.isAccrualActivity() && !lt.isInterestRefund();
}
@Override
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public BigDecimal calculateInterestRefundAmount(Long loanId, BigDecimal relatedRefundTransactionAmount,
LocalDate relatedRefundTransactionDate) {
public Money totalInterestByTransactions(LoanRepaymentScheduleTransactionProcessor processor, final Long loanId,
LocalDate relatedRefundTransactionDate, List<LoanTransaction> newTransactions, List<Long> oldTransactionIds) {
Loan loan = loanAssembler.assembleFrom(loanId);
BigDecimal totalInterestBeforeRefund = totalInterest(loan, BigDecimal.ZERO, relatedRefundTransactionDate);
BigDecimal totalInterestAfterRefund = totalInterest(loan, relatedRefundTransactionAmount, relatedRefundTransactionDate);
return totalInterestBeforeRefund.subtract(totalInterestAfterRefund);
if (processor == null) {
processor = loan.getTransactionProcessor();
}
if (!(processor instanceof AdvancedPaymentScheduleTransactionProcessor)) {
throw new IllegalArgumentException(
"Wrong processor implementation. ProgressiveLoanInterestRefundServiceImpl requires AdvancedPaymentScheduleTransactionProcessor");
}
List<LoanTransaction> transactionsToReprocess = new ArrayList<>();
List<LoanTransactionType> interestRefundTypes = loan.getSupportedInterestRefundTransactionTypes();
List<LoanTransaction> transactions = Stream.concat(loan.getLoanTransactions().stream() //
.filter(lt -> isTransactionNeededForInterestRefundCalculations(lt) //
&& oldTransactionIds.contains(lt.getId())), //
newTransactions.stream() //
.filter(this::isTransactionNeededForInterestRefundCalculations) //
.map(LoanTransaction::copyTransactionProperties)) //
.toList();
final AtomicReference<BigDecimal> refundFinal = new AtomicReference<>(
transactions.stream().filter(lt -> interestRefundTypes.contains(lt.getTypeOf())) //
.map(LoanTransaction::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
transactions.stream().filter(loanTransaction -> !interestRefundTypes.contains(loanTransaction.getTypeOf())) //
.forEach(lt -> simulateRepaymentForDisbursements(lt, refundFinal, transactionsToReprocess)); //
return recalculateTotalInterest((AdvancedPaymentScheduleTransactionProcessor) processor, loan, relatedRefundTransactionDate,
transactionsToReprocess);
}
@Override
public Money getTotalInterestRefunded(List<LoanTransaction> loanTransactions, MonetaryCurrency currency) {
return Money.of(currency, loanTransactions.stream().filter(LoanTransaction::isNotReversed).filter(LoanTransaction::isInterestRefund)
.map(LoanTransaction::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
}
}
@@ -31,11 +31,13 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestRefundServiceImpl;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration
public class LoanAccountAutoStarter {
@@ -104,8 +106,9 @@ public class LoanAccountAutoStarter {
@Bean
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator);
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator,
@Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, progressiveLoanInterestRefundService);
}
}