FINERACT-2104: Accrual Activity - Support final Accrual Activity transaction creation at the event of the loan got fully paid

This commit is contained in:
Soma Sörös
2024-07-24 14:48:34 +02:00
committed by Adam Saghy
parent 602ad4e953
commit b1798a66df
11 changed files with 851 additions and 64 deletions
@@ -18,15 +18,23 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
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.LoanTransactionType;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@@ -38,6 +46,8 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanWritePlatformService loanWritePlatformService;
private final ExternalIdFactory externalIdFactory;
private final BusinessEventNotifierService businessEventNotifierService;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
@@ -66,4 +76,98 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
return loan;
}
@Override
@Transactional
public void processAccrualActivityForLoanClosure(Loan loan) {
LocalDate date = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate();
List<LoanTransaction> accrualActivityTransaction = loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed)
.filter(LoanTransaction::isAccrualActivity).filter(loanTransaction -> loanTransaction.getDateOf().isAfter(date)).toList();
if (!accrualActivityTransaction.isEmpty()) {
accrualActivityTransaction.forEach(this::reverseAccrualActivityTransaction);
}
LoanTransaction loanTransaction = assembleClosingAccrualActivityTransaction(loan, date);
if (loanTransaction.getAmount().compareTo(BigDecimal.ZERO) != 0) {
loanWritePlatformService.makeAccrualActivityTransaction(loan, loanTransaction);
}
}
private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) {
loanTransaction.reverse();
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
private LoanTransaction assembleClosingAccrualActivityTransaction(Loan loan, LocalDate date) {
// collect fees
BigDecimal feeChargesPortion = BigDecimal.ZERO;
// collect penalties
BigDecimal penaltyChargesPortion = BigDecimal.ZERO;
// collect interests
BigDecimal interestPortion = BigDecimal.ZERO;
var currency = loan.getCurrency();
// sum up all accruals
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
feeChargesPortion = installment.getFeeAccrued(currency).getAmount().add(feeChargesPortion);
penaltyChargesPortion = installment.getPenaltyAccrued(currency).getAmount().add(penaltyChargesPortion);
interestPortion = installment.getInterestAccrued(currency).getAmount().add(interestPortion);
}
List<LoanTransaction> accrualActivities = loan.getLoanTransactions().stream().filter(LoanTransaction::isAccrualActivity)
.filter(LoanTransaction::isNotReversed).toList();
// subtract already Posted accruals
for (LoanTransaction accrualActivity : accrualActivities) {
if (accrualActivity.getFeeChargesPortion() != null) {
feeChargesPortion = feeChargesPortion.subtract(accrualActivity.getFeeChargesPortion());
}
if (accrualActivity.getPenaltyChargesPortion() != null) {
penaltyChargesPortion = penaltyChargesPortion.subtract(accrualActivity.getPenaltyChargesPortion());
}
if (accrualActivity.getInterestPortion() != null) {
interestPortion = interestPortion.subtract(accrualActivity.getInterestPortion());
}
}
BigDecimal transactionAmount = feeChargesPortion.add(penaltyChargesPortion).add(interestPortion);
ExternalId externalId = externalIdFactory.create();
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), date, transactionAmount, null,
interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId);
}
@Override
@Transactional
public void processAccrualActivityForLoanReopen(Loan loan) {
LoanTransaction lastAccrualActivityMarkedToReverse = null;
List<LoanTransaction> accrualActivityTransaction = loan.getLoanTransactions().stream()
.filter(loanTransaction -> loanTransaction.isNotReversed() && loanTransaction.isAccrualActivity())
.sorted(Comparator.comparing(LoanTransaction::getDateOf)).toList();
// grab the latest AccrualActivityTransaction
// it does not matter if it is on an installment due date or not because it was posted due to loan close
if (!accrualActivityTransaction.isEmpty()) {
lastAccrualActivityMarkedToReverse = accrualActivityTransaction.get(accrualActivityTransaction.size() - 1);
}
final LocalDate lastAccrualActivityTransactionDate = lastAccrualActivityMarkedToReverse == null ? null
: lastAccrualActivityMarkedToReverse.getDateOf();
LocalDate today = DateUtils.getBusinessLocalDate();
final List<LoanRepaymentScheduleInstallment> installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate = loan
.getRepaymentScheduleInstallments().stream()
.filter(installment -> installment.getDueDate().isBefore(today) && (lastAccrualActivityTransactionDate == null
|| installment.getDueDate().isAfter(lastAccrualActivityTransactionDate)
// if close event happened on installment due date
// we should reverse replay it to calculate installment related accrual parts only
|| installment.getDueDate().isEqual(lastAccrualActivityTransactionDate)))
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).toList();
for (LoanRepaymentScheduleInstallment installment : installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate) {
if (lastAccrualActivityMarkedToReverse != null) {
loanWritePlatformService.reverseReplayAccrualActivityTransaction(loan, lastAccrualActivityMarkedToReverse, installment,
installment.getDueDate());
lastAccrualActivityMarkedToReverse = null;
} else {
loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, installment.getDueDate());
}
}
if (lastAccrualActivityMarkedToReverse != null) {
reverseAccrualActivityTransaction(lastAccrualActivityMarkedToReverse);
}
}
}
@@ -32,10 +32,13 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService;
@PostConstruct
public void addListeners() {
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class, new LoanStatusChangedListener());
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class,
new LoanAccrualActivityPostingLoanStatusChangedListener());
}
private final class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
@@ -53,4 +56,21 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
}
}
}
private final class LoanAccrualActivityPostingLoanStatusChangedListener
implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
@Override
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
final Loan loan = event.get();
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
}
if (loan.isOpen()) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanReopen(loan);
}
}
}
}
}
@@ -1162,15 +1162,50 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
return changedTransactionDetail;
}
@Transactional
@Override
public Loan reverseReplayAccrualActivityTransaction(Loan loan, final LoanTransaction loanTransaction,
final LoanRepaymentScheduleInstallment installment, final LocalDate transactionDate) {
LoanTransaction newLoanTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (!newLoanTransaction.getDateOf().isEqual(loanTransaction.getDateOf())
|| !LoanTransaction.transactionAmountsMatch(loan.getCurrency(), loanTransaction, newLoanTransaction)) {
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newLoanTransaction);
loan.addLoanTransaction(newLoanTransaction);
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
data.setNewTransactionDetail(newLoanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
return loan;
}
@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, final LoanRepaymentScheduleInstallment installment,
final LocalDate transactionDate) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));
LoanTransaction newAccrualActivityTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (newAccrualActivityTransaction.getAmount().compareTo(BigDecimal.ZERO) == 0) {
return loan;
}
return makeAccrualActivityTransaction(loan, newAccrualActivityTransaction);
}
@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, LoanTransaction newAccrualActivityTransaction) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newAccrualActivityTransaction);
loan.addLoanTransaction(newAccrualActivityTransaction);
@@ -94,6 +94,7 @@ import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlat
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualActivityProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
@@ -329,8 +330,10 @@ public class LoanAccountConfiguration {
@Bean
@ConditionalOnMissingBean(LoanStatusChangePlatformService.class)
public LoanStatusChangePlatformService loanStatusChangePlatformService(BusinessEventNotifierService businessEventNotifierService,
LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualsProcessingService);
LoanAccrualsProcessingService loanAccrualsProcessingService,
LoanAccrualActivityProcessingService loanAccrualActivityProcessingService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualsProcessingService,
loanAccrualActivityProcessingService);
}
@Bean