FINERACT-2232: Capitalized income - amortization in case of Charge-off event

This commit is contained in:
mariiaKraievska
2025-05-19 21:56:48 +02:00
committed by Arnold Gálovics
parent 39062ad1a1
commit 4861ad19fd
14 changed files with 523 additions and 15 deletions
@@ -173,6 +173,16 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
private void createJournalEntriesForCapitalizedIncomeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
final Office office) {
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
if (isMarkedAsChargeOff) {
createJournalEntriesForChargeOffLoanCapitalizedIncomeAmortization(loanDTO, loanTransactionDTO, office);
} else {
createJournalEntriesForLoanCapitalizedIncomeAmortization(loanDTO, loanTransactionDTO, office);
}
}
private void createJournalEntriesForLoanCapitalizedIncomeAmortization(final LoanDTO loanDTO,
final LoanTransactionDTO loanTransactionDTO, final Office office) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
@@ -218,6 +228,73 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
}
}
private void createJournalEntriesForChargeOffLoanCapitalizedIncomeAmortization(final LoanDTO loanDTO,
final LoanTransactionDTO loanTransactionDTO, final Office office) {
// loan properties
final Long loanProductId = loanDTO.getLoanProductId();
final boolean isMarkedFraud = loanDTO.isMarkedAsFraud();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
// transaction properties
final String transactionId = loanTransactionDTO.getTransactionId();
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
final BigDecimal feesAmount = loanTransactionDTO.getFees();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
final Long chargeOffReasonCodeValue = loanDTO.getChargeOffReasonCodeValue();
final ProductToGLAccountMapping mapping = chargeOffReasonCodeValue != null
? helper.getChargeOffMappingByCodeValue(loanProductId, PortfolioProductType.LOAN, chargeOffReasonCodeValue)
: null;
if (mapping != null) {
final GLAccount accountDebit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), paymentTypeId);
// handle interest payment
if (MathUtil.isGreaterThanZero(interestAmount)) {
glAccountBalanceHolder.addToCredit(mapping.getGlAccount(), interestAmount);
glAccountBalanceHolder.addToDebit(accountDebit, interestAmount);
}
// handle fees payment
if (MathUtil.isGreaterThanZero(feesAmount)) {
glAccountBalanceHolder.addToCredit(mapping.getGlAccount(), feesAmount);
glAccountBalanceHolder.addToDebit(accountDebit, feesAmount);
}
} else {
final AccrualAccountsForLoan creditAccountType = isMarkedFraud ? AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE
: AccrualAccountsForLoan.CHARGE_OFF_EXPENSE;
// handle interest payment
if (MathUtil.isGreaterThanZero(interestAmount)) {
populateCreditDebitMaps(loanProductId, interestAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
}
// handle fees payment
if (MathUtil.isGreaterThanZero(feesAmount)) {
populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId, creditAccountType.getValue(),
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), glAccountBalanceHolder);
}
}
// create credit entries
for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
creditEntry.getValue(), glAccount);
}
}
// create debit entries
for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
if (MathUtil.isGreaterThanZero(debitEntry.getValue())) {
GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
debitEntry.getValue(), glAccount);
}
}
}
private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
Office office) {
final Long loanProductId = loanDTO.getLoanProductId();
@@ -24,9 +24,12 @@ import lombok.extern.slf4j.Slf4j;
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.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
@@ -39,6 +42,9 @@ public class LoanCapitalizedIncomeAmortizationEventService {
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());
}
private final class LoanCloseListener implements BusinessEventListener<LoanCloseBusinessEvent> {
@@ -68,4 +74,32 @@ public class LoanCapitalizedIncomeAmortizationEventService {
}
}
}
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().isEnableIncomeCapitalization() && loan.isChargedOff() && loanTransaction.isChargeOff()) {
log.debug("Loan charge-off on capitalized income amortization for loan {}", loan.getId());
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationOnLoanChargeOff(loan);
}
}
}
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().isEnableIncomeCapitalization() && loanTransaction.getTypeOf().isChargeOff()
&& !(loan.isChargedOff() || status.isClosedObligationsMet() || status.isClosedWrittenOff() || status.isOverpaid())) {
log.debug("Loan undo charge-off on capitalized income amortization for loan {}", loan.getId());
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationOnLoanUndoChargeOff(loanTransaction);
}
}
}
}
@@ -20,9 +20,12 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -42,31 +45,84 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
private final LoanTransactionRepository loanTransactionRepository;
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanJournalEntryPoster journalEntryPoster;
@Override
@Transactional
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
final LocalDate finalAmortizationTransactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
ExternalId externalId = ExternalId.empty();
BigDecimal totalUnrecognizedAmountCapitalizedIncomeAmount = BigDecimal.ZERO;
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, false);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
}
final boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled();
if (isExternalIdAutoGenerationEnabled) {
@Override
@Transactional
public void processCapitalizedIncomeAmortizationOnLoanChargeOff(@NotNull final Loan loan) {
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds();
LocalDate transactionDate = loan.getChargedOffOnDate();
if (transactionDate == null) {
transactionDate = DateUtils.getBusinessLocalDate();
}
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, true);
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
}
private LoanTransaction createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
final boolean isChargeOff) {
ExternalId externalId = ExternalId.empty();
BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO;
if (configurationDomainService.isExternalIdAutoGenerationEnabled()) {
externalId = ExternalId.generate();
}
for (LoanCapitalizedIncomeBalance balance : loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId())) {
totalUnrecognizedAmountCapitalizedIncomeAmount = totalUnrecognizedAmountCapitalizedIncomeAmount
.add(balance.getUnrecognizedAmount());
final List<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
for (LoanCapitalizedIncomeBalance balance : balances) {
final BigDecimal unrecognizedAmount = balance.getUnrecognizedAmount();
totalUnrecognizedAmount = totalUnrecognizedAmount.add(unrecognizedAmount);
if (isChargeOff) {
balance.setChargedOffAmount(unrecognizedAmount);
}
balance.setUnrecognizedAmount(BigDecimal.ZERO);
}
final LoanTransaction finalCapitalizedIncomeAmortization = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
finalAmortizationTransactionDate, totalUnrecognizedAmountCapitalizedIncomeAmount, externalId);
loan.addLoanTransaction(finalCapitalizedIncomeAmortization);
loanTransactionRepository.saveAndFlush(finalCapitalizedIncomeAmortization);
businessEventNotifierService.notifyPostBusinessEvent(
new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(finalCapitalizedIncomeAmortization));
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
transactionDate, totalUnrecognizedAmount, externalId);
loan.addLoanTransaction(amortizationTransaction);
loanTransactionRepository.saveAndFlush(amortizationTransaction);
return amortizationTransaction;
}
@Override
@Transactional
public void processCapitalizedIncomeAmortizationOnLoanUndoChargeOff(@NotNull final LoanTransaction loanTransaction) {
final Loan loan = loanTransaction.getLoan();
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds();
loan.getLoanTransactions().stream().filter(LoanTransaction::isCapitalizedIncomeAmortization)
.filter(transaction -> transaction.getTransactionDate().equals(loanTransaction.getTransactionDate()))
.forEach(transaction -> {
transaction.reverse();
final LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
});
for (LoanCapitalizedIncomeBalance balance : loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId())) {
balance.setUnrecognizedAmount(balance.getChargedOffAmount());
balance.setChargedOffAmount(BigDecimal.ZERO);
}
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
private LocalDate getFinalCapitalizedIncomeAmortizationTransactionDate(final Loan loan) {