FINERACT-2311: Buy Down fees - External business events

- buy down fee reversal fix
- added more listeners for buy down fee amortization on closed loans
This commit is contained in:
adam.magyari
2025-08-06 13:53:04 +02:00
committed by Adam Saghy
parent aa68f251d3
commit 6a198de0cc
17 changed files with 223 additions and 287 deletions
@@ -23,8 +23,11 @@ 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.LoanAdjustTransactionBusinessEvent;
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.LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeTransactionCreatedBusinessEvent;
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;
@@ -43,6 +46,12 @@ public class LoanBuyDownFeeAmortizationEventService {
@PostConstruct
public void addListeners() {
businessEventNotifierService.addPreBusinessEventListener(LoanCloseBusinessEvent.class, new LoanCloseListener());
businessEventNotifierService.addPostBusinessEventListener(LoanBuyDownFeeTransactionCreatedBusinessEvent.class,
new LoanBuyDownFeeTransactionListener());
businessEventNotifierService.addPostBusinessEventListener(LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent.class,
new LoanBuyDownFeeAdjustmentTransactionListener());
businessEventNotifierService.addPostBusinessEventListener(LoanAdjustTransactionBusinessEvent.class,
new LoanAdjustTransactionListener());
businessEventNotifierService.addPostBusinessEventListener(LoanBalanceChangedBusinessEvent.class, new LoanBalanceChangedListener());
businessEventNotifierService.addPostBusinessEventListener(LoanChargeOffPostBusinessEvent.class, new LoanChargeOffEventListener());
businessEventNotifierService.addPostBusinessEventListener(LoanUndoChargeOffBusinessEvent.class,
@@ -78,6 +87,52 @@ public class LoanBuyDownFeeAmortizationEventService {
}
}
private final class LoanBuyDownFeeTransactionListener implements BusinessEventListener<LoanBuyDownFeeTransactionCreatedBusinessEvent> {
@Override
public void onBusinessEvent(final LoanBuyDownFeeTransactionCreatedBusinessEvent event) {
final Loan loan = event.get().getLoan();
final LoanStatus status = loan.getStatus();
if (loan.getLoanProductRelatedDetail().isEnableBuyDownFee()
&& (status.isClosedObligationsMet() || status.isClosedWrittenOff() || status.isOverpaid())) {
log.debug("Loan buy down fee change on buy down fee amortization for closed loan {}", loan.getId());
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationOnLoanClosure(loan, true);
}
}
}
private final class LoanBuyDownFeeAdjustmentTransactionListener
implements BusinessEventListener<LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent> {
@Override
public void onBusinessEvent(final LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent event) {
final Loan loan = event.get().getLoan();
final LoanStatus status = loan.getStatus();
if (loan.getLoanProductRelatedDetail().isEnableBuyDownFee()
&& (status.isClosedObligationsMet() || status.isClosedWrittenOff() || status.isOverpaid())) {
log.debug("Loan buy down fee change on buy down fee amortization for closed loan {}", loan.getId());
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationOnLoanClosure(loan, true);
}
}
}
private final class LoanAdjustTransactionListener implements BusinessEventListener<LoanAdjustTransactionBusinessEvent> {
@Override
public void onBusinessEvent(final LoanAdjustTransactionBusinessEvent event) {
final LoanTransaction transactionToAdjust = event.get().getTransactionToAdjust();
final boolean isTransactionBuyDownFeeRelated = transactionToAdjust.isBuyDownFee()
|| transactionToAdjust.isBuyDownFeeAdjustment();
final Loan loan = transactionToAdjust.getLoan();
final LoanStatus status = loan.getStatus();
if (loan.getLoanProductRelatedDetail().isEnableBuyDownFee() && isTransactionBuyDownFeeRelated
&& (status.isClosedObligationsMet() || status.isClosedWrittenOff() || status.isOverpaid())) {
log.debug("Loan buy down fee change on buy down fee amortization for closed loan {}", loan.getId());
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationOnLoanClosure(loan, true);
}
}
}
private final class LoanChargeOffEventListener implements BusinessEventListener<LoanChargeOffPostBusinessEvent> {
@Override
@@ -24,7 +24,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
@@ -37,9 +36,6 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAdjustmentTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -177,6 +173,31 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
capitalizedIncomeBalance
.setUnrecognizedAmount(capitalizedIncomeBalance.getUnrecognizedAmount().add(transactionToAdjust.getAmount()));
}
if (transactionToAdjust.isBuyDownFee()) {
if (newTransactionDetail.isNotZero()) {
throw new InvalidLoanTransactionTypeException("transaction", "buy.down.fee.cannot.be.adjusted",
"Buy down fee transaction cannot be adjusted");
}
LoanBuyDownFeeBalance buyDownFeeBalance = loanBuyDownFeeBalanceRepository.findByLoanIdAndLoanTransactionId(loan.getId(),
transactionToAdjust.getId());
if (MathUtil.isGreaterThanZero(buyDownFeeBalance.getAmountAdjustment())) {
throw new InvalidLoanTransactionTypeException("transaction", "buy.down.fee.cannot.be.reversed.when.adjusted",
"Buy down fee transaction cannot be reversed when non-reversed adjustment exists for it.");
}
loanBuyDownFeeBalanceRepository.delete(buyDownFeeBalance);
}
if (transactionToAdjust.isBuyDownFeeAdjustment()) {
if (newTransactionDetail.isNotZero()) {
throw new InvalidLoanTransactionTypeException("transaction", "buy.down.fee.adjustment.cannot.be.adjusted",
"Buy down fee adjustment transaction cannot be adjusted");
}
LoanBuyDownFeeBalance buyDownFeeBalance = loanBuyDownFeeBalanceRepository.findBalanceForAdjustment(transactionToAdjust.getId());
buyDownFeeBalance.setAmountAdjustment(buyDownFeeBalance.getAmountAdjustment().subtract(transactionToAdjust.getAmount()));
buyDownFeeBalance.setUnrecognizedAmount(buyDownFeeBalance.getUnrecognizedAmount().add(transactionToAdjust.getAmount()));
}
LocalDate recalculateFrom = null;
@@ -292,9 +313,9 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
&& transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()
&& !transactionForAdjustment.isDeferredIncome() && !transactionForAdjustment.isCapitalizedIncomeAdjustment()
&& !transactionForAdjustment.isBuyDownFeeAdjustment()) {
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, accrual, credit balance refund, capitalized income, capitalized income adjustment or buy down fee adjustment can be adjusted.";
final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, accrual, credit balance refund, capitalized income, capitalized income adjustment, buy down fee or buy down fee adjustment can be adjusted.";
throw new InvalidLoanTransactionTypeException("transaction",
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.or.capitalizedIncome.or.capitalizedIncomeAdjustment.or.buyDownFeeAdjustment.transactions",
"adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.or.capitalizedIncome.or.capitalizedIncomeAdjustment.or.buyDownFee.or.buyDownFeeAdjustment.transactions",
errorMessage);
}
@@ -318,17 +339,6 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
});
} else if (transactionForAdjustment.isBuyDownFee()) {
Optional<LoanTransaction> optAmortizationAdjustmentTransaction = buyDownFeePlatformService
.reverseBuyDownFee(transactionForAdjustment);
if (optAmortizationAdjustmentTransaction.isPresent()) {
loan.addLoanTransaction(optAmortizationAdjustmentTransaction.get());
loanTransactionRepository.saveAndFlush(optAmortizationAdjustmentTransaction.get());
businessEventNotifierService
.notifyPostBusinessEvent(new LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent(
optAmortizationAdjustmentTransaction.get()));
}
businessEventNotifierService.notifyPostBusinessEvent(new LoanBuyDownFeeTransactionBusinessEvent(transactionForAdjustment));
}
if (loan.isClosedWrittenOff()) {
@@ -347,24 +357,6 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.CAPITALIZED_INCOME)) {
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.BUY_DOWN_FEE_ADJUSTMENT)) {
// adjust buy down fee balance
final LoanTransaction originalBuyDownFeeTransaction = transactionForAdjustment.getLoanTransactionRelations().stream()
.filter(r -> r.getRelationType().equals(LoanTransactionRelationTypeEnum.ADJUSTMENT)).findFirst().get()
.getToTransaction();
final LoanBuyDownFeeBalance buyDownFeeBalance = loanBuyDownFeeBalanceRepository.findByLoanIdAndLoanTransactionId(loan.getId(),
originalBuyDownFeeTransaction.getId());
if (buyDownFeeBalance != null) {
buyDownFeeBalance.setAmountAdjustment(
MathUtil.nullToZero(buyDownFeeBalance.getAmountAdjustment()).subtract(transactionForAdjustment.getAmount()));
buyDownFeeBalance.setUnrecognizedAmount(
MathUtil.nullToZero(buyDownFeeBalance.getUnrecognizedAmount()).add(transactionForAdjustment.getAmount()));
loanBuyDownFeeBalanceRepository.save(buyDownFeeBalance);
}
businessEventNotifierService
.notifyPostBusinessEvent(new LoanBuyDownFeeAdjustmentTransactionBusinessEvent(transactionForAdjustment));
}
}
private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
@@ -395,14 +395,10 @@ public class LoanAccountConfiguration {
LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository,
PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster loanJournalEntryPoster,
NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory,
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository,
ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService,
LoanLifecycleStateMachine loanLifecycleStateMachine, BusinessEventNotifierService businessEventNotifierService,
LoanTransactionRelationRepository loanTransactionRelationRepository) {
LoanBuyDownFeeBalanceRepository loanBuyDownFeeBalanceRepository, BusinessEventNotifierService businessEventNotifierService) {
return new BuyDownFeeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository,
paymentDetailWritePlatformService, loanJournalEntryPoster, noteWritePlatformService, externalIdFactory,
loanBuyDownFeeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService, loanLifecycleStateMachine,
businessEventNotifierService, loanTransactionRelationRepository);
loanBuyDownFeeBalanceRepository, businessEventNotifierService);
}
@Bean
@@ -211,5 +211,4 @@
<include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
<include file="parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml" relativeToChangelogFile="true" />
<include file="parts/0192_create_loan_approved_amount_history.xml" relativeToChangelogFile="true" />
<include file="parts/0193_buy_down_fee_events.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet id="4" author="fineract">
<update tableName="m_external_event_configuration">
<column name="type" value="LoanBuyDownFeeTransactionBusinessEvent"/>
<where>type='LoanBuyDownFeeTransactionCreatedBusinessEvent'</where>
</update>
<update tableName="m_external_event_configuration">
<column name="type" value="LoanBuyDownFeeAdjustmentTransactionBusinessEvent"/>
<where>type='LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent'</where>
</update>
</changeSet>
</databaseChangeLog>
@@ -110,7 +110,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
"LoanBuyDownFeeTransactionBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionBusinessEvent",
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");
@@ -204,7 +204,7 @@ public class ExternalEventConfigurationValidationServiceTest {
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
"LoanBuyDownFeeTransactionBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionBusinessEvent",
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent", "LoanApprovedAmountChangedBusinessEvent");