FINERACT-2104: Accrual Activity - Support final Accrual Activity transaction creation at the event of the loan got fully paid
This commit is contained in:
+104
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -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);
|
||||
|
||||
+5
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user