FINERACT-2311: Buy down fee - final amortization
- early repayment - write off - charge-off
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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 jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class LoanBuyDownFeeAmortizationEventService {
|
||||
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService;
|
||||
|
||||
@PostConstruct
|
||||
public void addListeners() {
|
||||
businessEventNotifierService.addPreBusinessEventListener(LoanCloseBusinessEvent.class, new LoanCloseListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanBalanceChangedBusinessEvent.class, new LoanBalanceChangedListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanChargeOffPostBusinessEvent.class, new LoanChargeOffEventListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanUndoChargeOffBusinessEvent.class,
|
||||
new LoanUndoChargeOffEventListener());
|
||||
businessEventNotifierService.addPreBusinessEventListener(LoanChargeOffPreBusinessEvent.class, new LoanChargeOffPreEventListener());
|
||||
}
|
||||
|
||||
private final class LoanCloseListener implements BusinessEventListener<LoanCloseBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(final LoanCloseBusinessEvent event) {
|
||||
final Loan loan = event.get();
|
||||
final LoanStatus status = loan.getStatus();
|
||||
if (loan.getLoanProductRelatedDetail().isEnableBuyDownFee()
|
||||
&& (status.isClosedObligationsMet() || status.isClosedWrittenOff() || status.isOverpaid())) {
|
||||
log.debug("Loan closure on buy down fee amortization for loan {}", loan.getId());
|
||||
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationOnLoanClosure(loan, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoanBalanceChangedListener implements BusinessEventListener<LoanBalanceChangedBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(final LoanBalanceChangedBusinessEvent event) {
|
||||
final Loan loan = event.get();
|
||||
final LoanStatus status = loan.getStatus();
|
||||
if (loan.getLoanProductRelatedDetail().isEnableBuyDownFee()
|
||||
&& (status.isClosedObligationsMet() || status.isClosedWrittenOff() || status.isOverpaid())) {
|
||||
log.debug("Loan balance change on buy down fee amortization for loan {}", loan.getId());
|
||||
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationOnLoanClosure(loan, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoanChargeOffEventListener implements BusinessEventListener<LoanChargeOffPostBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(final LoanChargeOffPostBusinessEvent event) {
|
||||
final LoanTransaction loanTransaction = event.get();
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
if (loan.getLoanProductRelatedDetail().isEnableBuyDownFee() && loan.isChargedOff() && loanTransaction.isChargeOff()) {
|
||||
log.debug("Loan charge-off on buy down fee amortization for loan {}", loan.getId());
|
||||
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationOnLoanChargeOff(loan, loanTransaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoanChargeOffPreEventListener implements BusinessEventListener<LoanChargeOffPreBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(final LoanChargeOffPreBusinessEvent event) {
|
||||
final Loan loan = event.get();
|
||||
if (loan.getLoanProductRelatedDetail().isEnableBuyDownFee()) {
|
||||
log.debug("Loan pre charge-off buy down fee amortization for loan {}", loan.getId());
|
||||
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationTillDate(loan, DateUtils.getBusinessLocalDate(),
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoanUndoChargeOffEventListener implements BusinessEventListener<LoanUndoChargeOffBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(final LoanUndoChargeOffBusinessEvent event) {
|
||||
final LoanTransaction loanTransaction = event.get();
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
final LoanStatus status = loan.getStatus();
|
||||
if (loan.getLoanProductRelatedDetail().isEnableBuyDownFee() && loanTransaction.getTypeOf().isChargeOff()
|
||||
&& !(loan.isChargedOff() || status.isClosedObligationsMet() || status.isClosedWrittenOff() || status.isOverpaid())) {
|
||||
log.debug("Loan undo charge-off on buy down fee amortization for loan {}", loan.getId());
|
||||
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationOnLoanUndoChargeOff(loanTransaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
@@ -20,11 +20,16 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
@@ -32,6 +37,8 @@ import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
|
||||
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.repository.LoanBuyDownFeeBalanceRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.util.BuyDownFeeAmortizationUtil;
|
||||
@@ -102,6 +109,127 @@ public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyD
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void processBuyDownFeeAmortizationOnLoanClosure(@NonNull final Loan loan, final boolean addJournal) {
|
||||
List<Long> existingTransactionIds = Collections.emptyList();
|
||||
List<Long> existingReversedTransactionIds = Collections.emptyList();
|
||||
if (addJournal) {
|
||||
existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
|
||||
existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
||||
}
|
||||
|
||||
final LocalDate transactionDate = getFinalBuyDownFeeAmortizationTransactionDate(loan);
|
||||
final Optional<LoanTransaction> amortizationTransaction = createBuyDownFeeAmortizationTransaction(loan, transactionDate, false,
|
||||
null);
|
||||
amortizationTransaction.ifPresent(loanTransaction -> {
|
||||
if (loanTransaction.isBuyDownFeeAmortization()) {
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent(loanTransaction));
|
||||
} else {
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent(loanTransaction));
|
||||
}
|
||||
});
|
||||
|
||||
if (addJournal) {
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void processBuyDownFeeAmortizationOnLoanChargeOff(@NonNull final Loan loan,
|
||||
@NonNull final LoanTransaction chargeOffTransaction) {
|
||||
final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
|
||||
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
||||
|
||||
LocalDate transactionDate = loan.getChargedOffOnDate();
|
||||
if (transactionDate == null) {
|
||||
transactionDate = DateUtils.getBusinessLocalDate();
|
||||
}
|
||||
|
||||
final Optional<LoanTransaction> amortizationTransaction = createBuyDownFeeAmortizationTransaction(loan, transactionDate, true,
|
||||
chargeOffTransaction);
|
||||
if (amortizationTransaction.isPresent()) {
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
if (amortizationTransaction.get().isBuyDownFeeAmortization()) {
|
||||
businessEventNotifierService.notifyPostBusinessEvent(
|
||||
new LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction.get()));
|
||||
} else {
|
||||
businessEventNotifierService.notifyPostBusinessEvent(
|
||||
new LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent(amortizationTransaction.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void processBuyDownFeeAmortizationOnLoanUndoChargeOff(@NonNull final LoanTransaction loanTransaction) {
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
|
||||
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
||||
|
||||
loan.getLoanTransactions().stream().filter(LoanTransaction::isBuyDownFeeAmortization)
|
||||
.filter(transaction -> transaction.getTransactionDate().equals(loanTransaction.getTransactionDate())
|
||||
&& transaction.getLoanTransactionRelations().stream()
|
||||
.anyMatch(rel -> LoanTransactionRelationTypeEnum.RELATED.equals(rel.getRelationType())
|
||||
&& rel.getToTransaction().equals(loanTransaction)))
|
||||
.forEach(transaction -> {
|
||||
transaction.reverse();
|
||||
final LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
|
||||
|
||||
});
|
||||
|
||||
for (LoanBuyDownFeeBalance balance : loanBuyDownFeeBalanceRepository.findAllByLoanId(loan.getId())) {
|
||||
balance.setUnrecognizedAmount(balance.getChargedOffAmount());
|
||||
balance.setChargedOffAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
|
||||
private Optional<LoanTransaction> createBuyDownFeeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
|
||||
final boolean isChargeOff, final LoanTransaction chargeOffTransaction) {
|
||||
ExternalId externalId = externalIdFactory.create();
|
||||
|
||||
final List<LoanBuyDownFeeBalance> balances = loanBuyDownFeeBalanceRepository.findAllByLoanId(loan.getId());
|
||||
|
||||
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
||||
for (LoanBuyDownFeeBalance balance : balances) {
|
||||
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
|
||||
LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate;
|
||||
final Money amortizationTillDate = BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
||||
maturityDate, loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), maturityDate, loan.getCurrency());
|
||||
totalAmortizationAmount = totalAmortizationAmount.add(amortizationTillDate.getAmount());
|
||||
if (isChargeOff) {
|
||||
balance.setChargedOffAmount(balance.getUnrecognizedAmount());
|
||||
}
|
||||
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan);
|
||||
BigDecimal totalUnrecognizedAmount = totalAmortizationAmount.subtract(amortizedAmount);
|
||||
if (MathUtil.isZero(totalUnrecognizedAmount)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final LoanTransaction amortizationTransaction = MathUtil.isGreaterThanZero(totalUnrecognizedAmount)
|
||||
? LoanTransaction.buyDownFeeAmortization(loan, loan.getOffice(), transactionDate, totalUnrecognizedAmount, externalId)
|
||||
: LoanTransaction.buyDownFeeAmortizationAdjustment(loan,
|
||||
Money.of(loan.getCurrency(), MathUtil.negate(totalUnrecognizedAmount)), transactionDate, externalId);
|
||||
if (isChargeOff) {
|
||||
amortizationTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(amortizationTransaction,
|
||||
chargeOffTransaction, LoanTransactionRelationTypeEnum.RELATED));
|
||||
}
|
||||
|
||||
loan.addLoanTransaction(amortizationTransaction);
|
||||
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
||||
|
||||
return Optional.of(amortizationTransaction);
|
||||
}
|
||||
|
||||
private LocalDate getFinalBuyDownFeeAmortizationTransactionDate(final Loan loan) {
|
||||
return switch (loan.getStatus()) {
|
||||
case CLOSED_OBLIGATIONS_MET -> loan.getClosedOnDate();
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ public class LoanCapitalizedIncomeAmortizationEventService {
|
||||
if (loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization()) {
|
||||
log.debug("Loan pre charge-off capitalized income amortization for loan {}", loan.getId());
|
||||
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationTillDate(loan,
|
||||
DateUtils.getBusinessLocalDate(), false);
|
||||
DateUtils.getBusinessLocalDate(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -123,6 +123,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBuyDownFeeAmortizationEventService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBuyDownFeeAmortizationProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBuyDownFeeAmortizationProcessingServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService;
|
||||
@@ -573,4 +574,12 @@ public class LoanAccountConfiguration {
|
||||
return new LoanBuyDownFeeAmortizationProcessingServiceImpl(loanTransactionRepository, loanBuyDownFeeBalanceRepository,
|
||||
businessEventNotifierService, journalEntryPoster, externalIdFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanBuyDownFeeAmortizationEventService.class)
|
||||
public LoanBuyDownFeeAmortizationEventService loanBuyDownFeeAmortizationEventService(
|
||||
BusinessEventNotifierService businessEventNotifierService,
|
||||
LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService) {
|
||||
return new LoanBuyDownFeeAmortizationEventService(businessEventNotifierService, loanBuyDownFeeAmortizationProcessingService);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user