FINERACT-2232: Capitalized income - Amortization rework
This commit is contained in:
+1
-1
@@ -1763,7 +1763,7 @@ public class LoanStepDef extends AbstractStepDef {
|
|||||||
final List<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
|
final List<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
|
||||||
final Optional<GetLoansLoanIdTransactions> transactionsMatch = transactions.stream()
|
final Optional<GetLoansLoanIdTransactions> transactionsMatch = transactions.stream()
|
||||||
.filter(t -> formatter.format(t.getDate()).equals(transactionDate) && t.getType().getCapitalizedIncomeAmortization())
|
.filter(t -> formatter.format(t.getDate()).equals(transactionDate) && t.getType().getCapitalizedIncomeAmortization())
|
||||||
.findFirst();
|
.reduce((one, two) -> two);
|
||||||
if (transactionsMatch.isPresent()) {
|
if (transactionsMatch.isPresent()) {
|
||||||
testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_AMORTIZATION_ID, transactionsMatch.get().getId());
|
testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_AMORTIZATION_ID, transactionsMatch.get().getId());
|
||||||
}
|
}
|
||||||
|
|||||||
+253
-254
File diff suppressed because it is too large
Load Diff
+19
@@ -348,4 +348,23 @@ public interface LoanTransactionRepository extends JpaRepository<LoanTransaction
|
|||||||
List<LoanTransaction> findNonReversedByLoanAndType(@Param("loan") Loan loan, @Param("type") LoanTransactionType type,
|
List<LoanTransaction> findNonReversedByLoanAndType(@Param("loan") Loan loan, @Param("type") LoanTransactionType type,
|
||||||
Pageable pageable);
|
Pageable pageable);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT COALESCE(SUM(CASE WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION THEN lt.amount
|
||||||
|
WHEN lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT THEN -lt.amount
|
||||||
|
ELSE 0 END), 0) FROM LoanTransaction lt
|
||||||
|
WHERE lt.loan = :loan
|
||||||
|
AND lt.reversed = false
|
||||||
|
AND (lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION
|
||||||
|
OR lt.typeOf = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT)
|
||||||
|
""")
|
||||||
|
BigDecimal getAmortizedAmount(@Param("loan") Loan loan);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT lt FROM LoanTransaction lt, LoanTransactionRelation ltr
|
||||||
|
WHERE lt.reversed = false
|
||||||
|
AND lt = ltr.fromTransaction
|
||||||
|
AND ltr.toTransaction = :capitalizedIncome
|
||||||
|
AND ltr.relationType = org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.ADJUSTMENT
|
||||||
|
""")
|
||||||
|
List<LoanTransaction> findAdjustmentsForCapitalizedIncome(@Param("capitalizedIncome") LoanTransaction capitalizedIncome);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.service;
|
package org.apache.fineract.portfolio.loanaccount.service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
@@ -26,7 +27,9 @@ public interface LoanCapitalizedIncomeAmortizationProcessingService {
|
|||||||
|
|
||||||
void processCapitalizedIncomeAmortizationOnLoanClosure(@NonNull Loan loan);
|
void processCapitalizedIncomeAmortizationOnLoanClosure(@NonNull Loan loan);
|
||||||
|
|
||||||
void processCapitalizedIncomeAmortizationOnLoanChargeOff(@NonNull Loan loan);
|
void processCapitalizedIncomeAmortizationOnLoanChargeOff(@NonNull Loan loan, @NonNull LoanTransaction chargeOffTransaction);
|
||||||
|
|
||||||
void processCapitalizedIncomeAmortizationOnLoanUndoChargeOff(@NonNull LoanTransaction loanTransaction);
|
void processCapitalizedIncomeAmortizationOnLoanUndoChargeOff(@NonNull LoanTransaction loanTransaction);
|
||||||
|
|
||||||
|
void processCapitalizedIncomeAmortizationTillDate(@NonNull Loan loan, @NonNull LocalDate tillDate);
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -142,7 +142,6 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc
|
|||||||
// Post journal entries
|
// Post journal entries
|
||||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|
||||||
// Accounting uses the original balance, balance update HAS TO HAPPEN AFTER postJournalEntries
|
|
||||||
LoanCapitalizedIncomeBalance capitalizedIncomeBalance = capitalizedIncomeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId,
|
LoanCapitalizedIncomeBalance capitalizedIncomeBalance = capitalizedIncomeBalanceRepository.findByLoanIdAndLoanTransactionId(loanId,
|
||||||
capitalizedIncomeTransactionId);
|
capitalizedIncomeTransactionId);
|
||||||
capitalizedIncomeBalance
|
capitalizedIncomeBalance
|
||||||
|
|||||||
+1
-46
@@ -21,12 +21,10 @@ package org.apache.fineract.accounting.journalentry.service;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
||||||
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
|
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
|
||||||
@@ -36,19 +34,12 @@ import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
|||||||
import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder;
|
import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder;
|
||||||
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
|
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
|
||||||
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
|
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
|
||||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
|
||||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
|
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
|
||||||
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
|
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
|
||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
|
||||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
|
||||||
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.domain.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -246,46 +237,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve Debit
|
// Resolve Debit
|
||||||
GLAccount accountIncomeFromCapitalization = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
|
||||||
AccrualAccountsForLoan.INCOME_FROM_CAPITALIZATION.getValue(), paymentTypeId);
|
|
||||||
GLAccount accountDeferredIncome = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
GLAccount accountDeferredIncome = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||||
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), paymentTypeId);
|
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), paymentTypeId);
|
||||||
|
|
||||||
Optional<LoanTransaction> transactionOpt = loanTransactionRepository.findById(Long.parseLong(transactionId));
|
glAccountBalanceHolder.addToDebit(accountDeferredIncome, transactionAmount);
|
||||||
if (transactionOpt.isEmpty()) {
|
|
||||||
throw new PlatformDataIntegrityException("transaction.for.accounting.not.found", "Transaction for accounting not found");
|
|
||||||
}
|
|
||||||
Optional<LoanTransactionRelation> relationToOriginalTransactionOpt = transactionOpt.get().getLoanTransactionRelations().stream()//
|
|
||||||
.filter(rel -> LoanTransactionRelationTypeEnum.REPLAYED.equals(rel.getRelationType()))//
|
|
||||||
.min(Comparator.comparing(AbstractPersistableCustom::getId));
|
|
||||||
if (relationToOriginalTransactionOpt.isPresent()) {
|
|
||||||
// Reverse-replay case
|
|
||||||
final LoanTransaction originalTransaction = relationToOriginalTransactionOpt.get().getToTransaction();
|
|
||||||
final List<JournalEntry> originalJournalEntries = this.journalEntryRepository.findJournalEntries(
|
|
||||||
AccountingProcessorHelper.LOAN_TRANSACTION_IDENTIFIER + originalTransaction.getId(),
|
|
||||||
PortfolioProductType.LOAN.getValue());
|
|
||||||
|
|
||||||
for (JournalEntry originalJournalEntry : originalJournalEntries) {
|
|
||||||
if (originalJournalEntry.isDebitEntry()) {
|
|
||||||
if (accountIncomeFromCapitalization.getId().equals(originalJournalEntry.getGlAccount().getId())) {
|
|
||||||
glAccountBalanceHolder.addToDebit(accountIncomeFromCapitalization, originalJournalEntry.getAmount());
|
|
||||||
} else if (accountDeferredIncome.getId().equals(originalJournalEntry.getGlAccount().getId())) {
|
|
||||||
glAccountBalanceHolder.addToDebit(accountDeferredIncome, originalJournalEntry.getAmount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LoanCapitalizedIncomeBalance capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository
|
|
||||||
.findBalanceForAdjustment(Long.parseLong(loanTransactionDTO.getTransactionId()));
|
|
||||||
if (MathUtil.isGreaterThan(transactionAmount, capitalizedIncomeBalance.getUnrecognizedAmount())) {
|
|
||||||
BigDecimal amortizedAmount = transactionAmount.subtract(capitalizedIncomeBalance.getUnrecognizedAmount());
|
|
||||||
|
|
||||||
glAccountBalanceHolder.addToDebit(accountIncomeFromCapitalization, amortizedAmount);
|
|
||||||
glAccountBalanceHolder.addToDebit(accountDeferredIncome, transactionAmount.subtract(amortizedAmount));
|
|
||||||
} else {
|
|
||||||
glAccountBalanceHolder.addToDebit(accountDeferredIncome, transactionAmount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create credit entries
|
// create credit entries
|
||||||
|
|||||||
+3
-61
@@ -19,30 +19,12 @@
|
|||||||
package org.apache.fineract.cob.loan;
|
package org.apache.fineract.cob.loan;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.MathContext;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
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.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
|
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
|
||||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.util.CapitalizedIncomeAmortizationUtil;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -50,54 +32,14 @@ import org.springframework.stereotype.Component;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class CapitalizedIncomeAmortizationBusinessStep implements LoanCOBBusinessStep {
|
public class CapitalizedIncomeAmortizationBusinessStep implements LoanCOBBusinessStep {
|
||||||
|
|
||||||
private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository;
|
private final LoanCapitalizedIncomeAmortizationProcessingService loanCapitalizedIncomeAmortizationProcessingService;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
|
||||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
|
||||||
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
|
|
||||||
private final LoanTransactionRepository loanTransactionRepository;
|
|
||||||
private final ExternalIdFactory externalIdFactory;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public Loan execute(Loan loan) {
|
public Loan execute(Loan loan) {
|
||||||
MathContext mc = MoneyHelper.getMathContext();
|
|
||||||
LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||||
List<LoanCapitalizedIncomeBalance> balances = capitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
|
|
||||||
|
|
||||||
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationTillDate(loan, businessDate);
|
||||||
for (LoanCapitalizedIncomeBalance balance : balances) {
|
|
||||||
if (DateUtils.isAfterInclusive(businessDate, balance.getDate())) {
|
|
||||||
long daysUntilMaturity = businessDate.until(loan.getMaturityDate(), ChronoUnit.DAYS);
|
|
||||||
Money dailyAmortization = CapitalizedIncomeAmortizationUtil.calculateDailyAmortization(
|
|
||||||
loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), daysUntilMaturity,
|
|
||||||
balance.getUnrecognizedAmount(), loan.getCurrency().toData());
|
|
||||||
totalAmortizationAmount = totalAmortizationAmount.add(dailyAmortization.getAmount(), mc);
|
|
||||||
balance.setUnrecognizedAmount(balance.getUnrecognizedAmount().subtract(dailyAmortization.getAmount(), mc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
capitalizedIncomeBalanceRepository.saveAll(balances);
|
|
||||||
|
|
||||||
if (MathUtil.isGreaterThanZero(totalAmortizationAmount)) {
|
|
||||||
LoanTransaction transaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(), businessDate,
|
|
||||||
totalAmortizationAmount, externalIdFactory.create());
|
|
||||||
loan.addLoanTransaction(transaction);
|
|
||||||
|
|
||||||
transaction = loanTransactionRepository.save(transaction);
|
|
||||||
loanTransactionRepository.flush();
|
|
||||||
|
|
||||||
final AccountingBridgeLoanTransactionDTO transactionDTO = loanAccountingBridgeMapper.mapToLoanTransactionData(transaction,
|
|
||||||
loan.getCurrency().getCode());
|
|
||||||
final AccountingBridgeDataDTO accountingBridgeData = new AccountingBridgeDataDTO(loan.getId(), loan.getLoanProduct().getId(),
|
|
||||||
loan.getOfficeId(), loan.getCurrencyCode(), loan.getSummary().getTotalInterestCharged(),
|
|
||||||
loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct(),
|
|
||||||
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(), false,
|
|
||||||
false, false, null, loan.isClosedWrittenOff(), List.of(transactionDTO));
|
|
||||||
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
|
|
||||||
|
|
||||||
businessEventNotifierService
|
|
||||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(transaction));
|
|
||||||
}
|
|
||||||
|
|
||||||
return loan;
|
return loan;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-1
@@ -21,10 +21,12 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
|||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.BusinessEventListener;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
|
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.LoanCloseBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
|
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.domain.loan.transaction.LoanUndoChargeOffBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
@@ -45,6 +47,7 @@ public class LoanCapitalizedIncomeAmortizationEventService {
|
|||||||
businessEventNotifierService.addPostBusinessEventListener(LoanChargeOffPostBusinessEvent.class, new LoanChargeOffEventListener());
|
businessEventNotifierService.addPostBusinessEventListener(LoanChargeOffPostBusinessEvent.class, new LoanChargeOffEventListener());
|
||||||
businessEventNotifierService.addPostBusinessEventListener(LoanUndoChargeOffBusinessEvent.class,
|
businessEventNotifierService.addPostBusinessEventListener(LoanUndoChargeOffBusinessEvent.class,
|
||||||
new LoanUndoChargeOffEventListener());
|
new LoanUndoChargeOffEventListener());
|
||||||
|
businessEventNotifierService.addPreBusinessEventListener(LoanChargeOffPreBusinessEvent.class, new LoanChargeOffPreEventListener());
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class LoanCloseListener implements BusinessEventListener<LoanCloseBusinessEvent> {
|
private final class LoanCloseListener implements BusinessEventListener<LoanCloseBusinessEvent> {
|
||||||
@@ -83,7 +86,21 @@ public class LoanCapitalizedIncomeAmortizationEventService {
|
|||||||
final Loan loan = loanTransaction.getLoan();
|
final Loan loan = loanTransaction.getLoan();
|
||||||
if (loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization() && loan.isChargedOff() && loanTransaction.isChargeOff()) {
|
if (loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization() && loan.isChargedOff() && loanTransaction.isChargeOff()) {
|
||||||
log.debug("Loan charge-off on capitalized income amortization for loan {}", loan.getId());
|
log.debug("Loan charge-off on capitalized income amortization for loan {}", loan.getId());
|
||||||
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationOnLoanChargeOff(loan);
|
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationOnLoanChargeOff(loan,
|
||||||
|
loanTransaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class LoanChargeOffPreEventListener implements BusinessEventListener<LoanChargeOffPreBusinessEvent> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBusinessEvent(final LoanChargeOffPreBusinessEvent event) {
|
||||||
|
final Loan loan = event.get();
|
||||||
|
if (loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization()) {
|
||||||
|
log.debug("Loan pre charge-off capitalized income amortization for loan {}", loan.getId());
|
||||||
|
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationTillDate(loan,
|
||||||
|
DateUtils.getBusinessLocalDate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-16
@@ -26,16 +26,24 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
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.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.LoanAdjustTransactionBusinessEvent;
|
||||||
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
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.domain.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.util.CapitalizedIncomeAmortizationUtil;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -48,20 +56,29 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final LoanJournalEntryPoster journalEntryPoster;
|
private final LoanJournalEntryPoster journalEntryPoster;
|
||||||
|
private final ExternalIdFactory externalIdFactory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
||||||
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
||||||
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
||||||
false);
|
false, null);
|
||||||
amortizationTransaction.ifPresent(loanTransaction -> businessEventNotifierService
|
amortizationTransaction.ifPresent(loanTransaction -> {
|
||||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(loanTransaction)));
|
if (loanTransaction.isCapitalizedIncomeAmortization()) {
|
||||||
|
businessEventNotifierService
|
||||||
|
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(loanTransaction));
|
||||||
|
} else {
|
||||||
|
businessEventNotifierService.notifyPostBusinessEvent(
|
||||||
|
new LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent(loanTransaction));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void processCapitalizedIncomeAmortizationOnLoanChargeOff(@NotNull final Loan loan) {
|
public void processCapitalizedIncomeAmortizationOnLoanChargeOff(@NotNull final Loan loan,
|
||||||
|
@NonNull final LoanTransaction chargeOffTransaction) {
|
||||||
final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
|
final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
|
||||||
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
||||||
|
|
||||||
@@ -71,39 +88,57 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
final Optional<LoanTransaction> amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate,
|
||||||
true);
|
true, chargeOffTransaction);
|
||||||
if (amortizationTransaction.isPresent()) {
|
if (amortizationTransaction.isPresent()) {
|
||||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(
|
if (amortizationTransaction.get().isCapitalizedIncomeAmortization()) {
|
||||||
new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction.get()));
|
businessEventNotifierService.notifyPostBusinessEvent(
|
||||||
|
new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction.get()));
|
||||||
|
} else {
|
||||||
|
businessEventNotifierService.notifyPostBusinessEvent(
|
||||||
|
new LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent(amortizationTransaction.get()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<LoanTransaction> createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
|
private Optional<LoanTransaction> createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
|
||||||
final boolean isChargeOff) {
|
final boolean isChargeOff, final LoanTransaction chargeOffTransaction) {
|
||||||
ExternalId externalId = ExternalId.empty();
|
ExternalId externalId = ExternalId.empty();
|
||||||
BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
if (configurationDomainService.isExternalIdAutoGenerationEnabled()) {
|
if (configurationDomainService.isExternalIdAutoGenerationEnabled()) {
|
||||||
externalId = ExternalId.generate();
|
externalId = ExternalId.generate();
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
|
final List<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
|
||||||
|
|
||||||
|
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
||||||
for (LoanCapitalizedIncomeBalance balance : balances) {
|
for (LoanCapitalizedIncomeBalance balance : balances) {
|
||||||
final BigDecimal unrecognizedAmount = balance.getUnrecognizedAmount();
|
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustmentsForCapitalizedIncome(balance.getLoanTransaction());
|
||||||
totalUnrecognizedAmount = totalUnrecognizedAmount.add(unrecognizedAmount);
|
LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate;
|
||||||
|
final Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
||||||
|
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), maturityDate, loan.getCurrency());
|
||||||
|
totalAmortizationAmount = totalAmortizationAmount.add(amortizationTillDate.getAmount());
|
||||||
if (isChargeOff) {
|
if (isChargeOff) {
|
||||||
balance.setChargedOffAmount(unrecognizedAmount);
|
balance.setChargedOffAmount(balance.getUnrecognizedAmount());
|
||||||
}
|
}
|
||||||
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MathUtil.isLessThanOrEqualZero(totalUnrecognizedAmount)) {
|
BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmount(loan);
|
||||||
|
BigDecimal totalUnrecognizedAmount = totalAmortizationAmount.subtract(amortizedAmount);
|
||||||
|
if (MathUtil.isZero(totalUnrecognizedAmount)) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
|
final LoanTransaction amortizationTransaction = MathUtil.isGreaterThanZero(totalUnrecognizedAmount)
|
||||||
transactionDate, totalUnrecognizedAmount, externalId);
|
? LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(), transactionDate, totalUnrecognizedAmount,
|
||||||
|
externalId)
|
||||||
|
: LoanTransaction.capitalizedIncomeAmortizationAdjustment(loan,
|
||||||
|
Money.of(loan.getCurrency(), MathUtil.negate(totalUnrecognizedAmount)), transactionDate, externalId);
|
||||||
|
if (isChargeOff) {
|
||||||
|
amortizationTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(amortizationTransaction,
|
||||||
|
chargeOffTransaction, LoanTransactionRelationTypeEnum.RELATED));
|
||||||
|
}
|
||||||
|
|
||||||
loan.addLoanTransaction(amortizationTransaction);
|
loan.addLoanTransaction(amortizationTransaction);
|
||||||
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
||||||
@@ -119,7 +154,10 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
||||||
|
|
||||||
loan.getLoanTransactions().stream().filter(LoanTransaction::isCapitalizedIncomeAmortization)
|
loan.getLoanTransactions().stream().filter(LoanTransaction::isCapitalizedIncomeAmortization)
|
||||||
.filter(transaction -> transaction.getTransactionDate().equals(loanTransaction.getTransactionDate()))
|
.filter(transaction -> transaction.getTransactionDate().equals(loanTransaction.getTransactionDate())
|
||||||
|
&& transaction.getLoanTransactionRelations().stream()
|
||||||
|
.anyMatch(rel -> LoanTransactionRelationTypeEnum.RELATED.equals(rel.getRelationType())
|
||||||
|
&& rel.getToTransaction().equals(loanTransaction)))
|
||||||
.forEach(transaction -> {
|
.forEach(transaction -> {
|
||||||
transaction.reverse();
|
transaction.reverse();
|
||||||
final LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction);
|
final LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction);
|
||||||
@@ -135,6 +173,57 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void processCapitalizedIncomeAmortizationTillDate(@NonNull Loan loan, @NonNull LocalDate tillDate) {
|
||||||
|
final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
|
||||||
|
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
||||||
|
|
||||||
|
List<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
|
||||||
|
|
||||||
|
LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate()
|
||||||
|
: getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
||||||
|
LocalDate tillDatePlusOne = tillDate.plusDays(1);
|
||||||
|
if (tillDatePlusOne.isAfter(maturityDate)) {
|
||||||
|
tillDatePlusOne = maturityDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Money totalAmortization = Money.zero(loan.getCurrency());
|
||||||
|
for (LoanCapitalizedIncomeBalance balance : balances) {
|
||||||
|
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustmentsForCapitalizedIncome(balance.getLoanTransaction());
|
||||||
|
Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
||||||
|
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), tillDatePlusOne, loan.getCurrency());
|
||||||
|
totalAmortization = totalAmortization.add(amortizationTillDate);
|
||||||
|
|
||||||
|
balance.setUnrecognizedAmount(balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment()))
|
||||||
|
.subtract(amortizationTillDate.getAmount()));
|
||||||
|
}
|
||||||
|
|
||||||
|
loanCapitalizedIncomeBalanceRepository.saveAll(balances);
|
||||||
|
|
||||||
|
BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmount(loan);
|
||||||
|
BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized);
|
||||||
|
|
||||||
|
if (!MathUtil.isZero(totalAmortizationAmount)) {
|
||||||
|
LoanTransaction transaction = MathUtil.isGreaterThanZero(totalAmortizationAmount)
|
||||||
|
? LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(), tillDate, totalAmortizationAmount,
|
||||||
|
externalIdFactory.create())
|
||||||
|
: LoanTransaction.capitalizedIncomeAmortizationAdjustment(loan,
|
||||||
|
Money.of(loan.getCurrency(), MathUtil.negate(totalAmortizationAmount)), tillDate, externalIdFactory.create());
|
||||||
|
loan.addLoanTransaction(transaction);
|
||||||
|
|
||||||
|
transaction = loanTransactionRepository.save(transaction);
|
||||||
|
loanTransactionRepository.flush();
|
||||||
|
|
||||||
|
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||||
|
|
||||||
|
BusinessEvent<?> event = MathUtil.isGreaterThanZero(totalAmortizationAmount)
|
||||||
|
? new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(transaction)
|
||||||
|
: new LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent(transaction);
|
||||||
|
businessEventNotifierService.notifyPostBusinessEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private LocalDate getFinalCapitalizedIncomeAmortizationTransactionDate(final Loan loan) {
|
private LocalDate getFinalCapitalizedIncomeAmortizationTransactionDate(final Loan loan) {
|
||||||
return switch (loan.getStatus()) {
|
return switch (loan.getStatus()) {
|
||||||
case CLOSED_OBLIGATIONS_MET -> loan.getClosedOnDate();
|
case CLOSED_OBLIGATIONS_MET -> loan.getClosedOnDate();
|
||||||
|
|||||||
-13
@@ -37,7 +37,6 @@ import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
|||||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
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.LoanAdjustTransactionBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent;
|
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
@@ -156,9 +155,6 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
|||||||
throw new InvalidLoanTransactionTypeException("transaction", "capitalizedIncome.cannot.be.reversed.when.adjusted",
|
throw new InvalidLoanTransactionTypeException("transaction", "capitalizedIncome.cannot.be.reversed.when.adjusted",
|
||||||
"Capitalized income transaction cannot be reversed when non-reversed adjustment exists for it.");
|
"Capitalized income transaction cannot be reversed when non-reversed adjustment exists for it.");
|
||||||
}
|
}
|
||||||
BigDecimal recognizedAmount = capitalizedIncomeBalance.getAmount().subtract(capitalizedIncomeBalance.getUnrecognizedAmount());
|
|
||||||
capitalizedIncomeAmortizationAdjustmentTransaction = LoanTransaction.capitalizedIncomeAmortizationAdjustment(loan,
|
|
||||||
Money.of(loan.getCurrency(), recognizedAmount), DateUtils.getBusinessLocalDate(), externalIdFactory.create());
|
|
||||||
loanCapitalizedIncomeBalanceRepository.delete(capitalizedIncomeBalance);
|
loanCapitalizedIncomeBalanceRepository.delete(capitalizedIncomeBalance);
|
||||||
}
|
}
|
||||||
if (transactionToAdjust.isCapitalizedIncomeAdjustment()) {
|
if (transactionToAdjust.isCapitalizedIncomeAdjustment()) {
|
||||||
@@ -213,15 +209,6 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
|
|||||||
this.loanTransactionRepository.saveAndFlush(newTransactionDetail);
|
this.loanTransactionRepository.saveAndFlush(newTransactionDetail);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (capitalizedIncomeAmortizationAdjustmentTransaction != null && capitalizedIncomeAmortizationAdjustmentTransaction.isNotZero()) {
|
|
||||||
LoanTransaction savedCapitalizedIncomeAmortizationAdjustmentTransaction = loanTransactionRepository
|
|
||||||
.saveAndFlush(capitalizedIncomeAmortizationAdjustmentTransaction);
|
|
||||||
loan.addLoanTransaction(savedCapitalizedIncomeAmortizationAdjustmentTransaction);
|
|
||||||
businessEventNotifierService
|
|
||||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent(
|
|
||||||
capitalizedIncomeAmortizationAdjustmentTransaction));
|
|
||||||
}
|
|
||||||
|
|
||||||
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(noteText)) {
|
if (StringUtils.isNotBlank(noteText)) {
|
||||||
|
|||||||
+51
-6
@@ -19,21 +19,66 @@
|
|||||||
package org.apache.fineract.portfolio.loanaccount.util;
|
package org.apache.fineract.portfolio.loanaccount.util;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
import java.time.LocalDate;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
|
|
||||||
public final class CapitalizedIncomeAmortizationUtil {
|
public final class CapitalizedIncomeAmortizationUtil {
|
||||||
|
|
||||||
private CapitalizedIncomeAmortizationUtil() {}
|
private CapitalizedIncomeAmortizationUtil() {}
|
||||||
|
|
||||||
public static Money calculateDailyAmortization(final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy,
|
public static Money calculateTotalAmortizationTillDate(final LoanCapitalizedIncomeBalance capitalizedIncomeBalance,
|
||||||
final long daysTillMaturity, final BigDecimal remainingAmount, final CurrencyData currencyData) {
|
final List<LoanTransaction> adjustmentTransactions, final LocalDate maturityDate,
|
||||||
BigDecimal amortization = switch (capitalizedIncomeStrategy) {
|
final LoanCapitalizedIncomeStrategy capitalizedIncomeStrategy, final LocalDate tillDate, final MonetaryCurrency currency) {
|
||||||
case EQUAL_AMORTIZATION -> remainingAmount.divide(BigDecimal.valueOf(daysTillMaturity), MoneyHelper.getMathContext());
|
return switch (capitalizedIncomeStrategy) {
|
||||||
|
case EQUAL_AMORTIZATION -> calculateTotalAmortizationTillDateEqualAmortization(capitalizedIncomeBalance, adjustmentTransactions,
|
||||||
|
maturityDate, tillDate, currency);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return Money.of(currencyData, amortization);
|
private static Money calculateTotalAmortizationTillDateEqualAmortization(LoanCapitalizedIncomeBalance balance,
|
||||||
|
List<LoanTransaction> adjustmentTransactions, LocalDate maturityDate, LocalDate tillDate, MonetaryCurrency currency) {
|
||||||
|
|
||||||
|
BigDecimal unrecognizedAmount = balance.getAmount();
|
||||||
|
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
||||||
|
BigDecimal overAmortizationCorrection = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
List<LoanTransaction> sortedAdjustmentTransactions = adjustmentTransactions.stream()
|
||||||
|
.sorted(Comparator.comparing(LoanTransaction::getDateOf)).toList();
|
||||||
|
LocalDate periodStart = balance.getDate();
|
||||||
|
for (LoanTransaction adjustmentTransaction : sortedAdjustmentTransactions) {
|
||||||
|
long daysUntilMaturity = DateUtils.getDifferenceInDays(periodStart, maturityDate);
|
||||||
|
long daysOfPeriod = DateUtils.getDifferenceInDays(periodStart, adjustmentTransaction.getDateOf());
|
||||||
|
BigDecimal periodAmortization = daysUntilMaturity == 0L ? BigDecimal.ZERO
|
||||||
|
: unrecognizedAmount.multiply(BigDecimal.valueOf(daysOfPeriod)).divide(BigDecimal.valueOf(daysUntilMaturity),
|
||||||
|
MoneyHelper.getMathContext());
|
||||||
|
|
||||||
|
totalAmortizationAmount = totalAmortizationAmount.add(periodAmortization);
|
||||||
|
unrecognizedAmount = unrecognizedAmount.subtract(periodAmortization).subtract(adjustmentTransaction.getAmount());
|
||||||
|
if (MathUtil.isLessThanZero(unrecognizedAmount)) {
|
||||||
|
overAmortizationCorrection = overAmortizationCorrection.add(unrecognizedAmount);
|
||||||
|
unrecognizedAmount = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
periodStart = adjustmentTransaction.getDateOf();
|
||||||
|
}
|
||||||
|
if (periodStart.isBefore(tillDate)) {
|
||||||
|
long daysUntilMaturity = DateUtils.getDifferenceInDays(periodStart, maturityDate);
|
||||||
|
long daysOfPeriod = DateUtils.getDifferenceInDays(periodStart, tillDate);
|
||||||
|
BigDecimal periodAmortization = unrecognizedAmount.multiply(BigDecimal.valueOf(daysOfPeriod))
|
||||||
|
.divide(BigDecimal.valueOf(daysUntilMaturity), MoneyHelper.getMathContext());
|
||||||
|
totalAmortizationAmount = totalAmortizationAmount.add(periodAmortization);
|
||||||
|
} else if (balance.getDate().equals(maturityDate)) {
|
||||||
|
totalAmortizationAmount = totalAmortizationAmount.add(unrecognizedAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Money.of(currency, totalAmortizationAmount.add(overAmortizationCorrection));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-66
@@ -1,66 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.math.MathContext;
|
|
||||||
import java.math.RoundingMode;
|
|
||||||
import org.apache.fineract.organisation.monetary.data.CurrencyData;
|
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
|
||||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeStrategy;
|
|
||||||
import org.junit.jupiter.api.AfterAll;
|
|
||||||
import org.junit.jupiter.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.MockedStatic;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
|
|
||||||
class CapitalizedIncomeAmortizationUtilTest {
|
|
||||||
|
|
||||||
private static final MockedStatic<MoneyHelper> MONEY_HELPER = Mockito.mockStatic(MoneyHelper.class);
|
|
||||||
|
|
||||||
@BeforeAll
|
|
||||||
public static void init() {
|
|
||||||
MONEY_HELPER.when(MoneyHelper::getMathContext).thenReturn(new MathContext(19, RoundingMode.HALF_EVEN));
|
|
||||||
MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterAll
|
|
||||||
public static void deregister() {
|
|
||||||
MONEY_HELPER.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void calculateDailyAmortizationEqualAmortization() {
|
|
||||||
CurrencyData currency = new CurrencyData("USD", 2, null);
|
|
||||||
|
|
||||||
BigDecimal remaining = BigDecimal.valueOf(50.0);
|
|
||||||
Money dailyAmortization = CapitalizedIncomeAmortizationUtil
|
|
||||||
.calculateDailyAmortization(LoanCapitalizedIncomeStrategy.EQUAL_AMORTIZATION, 91L, remaining, currency);
|
|
||||||
|
|
||||||
Assertions.assertEquals(BigDecimal.valueOf(0.55), dailyAmortization.getAmount());
|
|
||||||
|
|
||||||
BigDecimal remaining2 = BigDecimal.valueOf(8.2);
|
|
||||||
Money dailyAmortization2 = CapitalizedIncomeAmortizationUtil
|
|
||||||
.calculateDailyAmortization(LoanCapitalizedIncomeStrategy.EQUAL_AMORTIZATION, 15L, remaining2, currency);
|
|
||||||
|
|
||||||
Assertions.assertEquals(BigDecimal.valueOf(0.55), dailyAmortization2.getAmount());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+25
-9
@@ -232,6 +232,7 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
public void testLoanCapitalizedIncomeAdjustmentWithAmortizationAccounting() {
|
public void testLoanCapitalizedIncomeAdjustmentWithAmortizationAccounting() {
|
||||||
final AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
final AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||||
final AtomicReference<Long> capitalizedIncomeIdRef = new AtomicReference<>();
|
final AtomicReference<Long> capitalizedIncomeIdRef = new AtomicReference<>();
|
||||||
|
final AtomicReference<Long> capitalizedIncomeAdjustmentTransactionIdRef = new AtomicReference<>();
|
||||||
|
|
||||||
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||||
|
|
||||||
@@ -290,6 +291,11 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
|
|
||||||
Long capitalizedIncomeAdjustmentTransactionId = loanTransactionHelper
|
Long capitalizedIncomeAdjustmentTransactionId = loanTransactionHelper
|
||||||
.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 100.0).getResourceId();
|
.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 100.0).getResourceId();
|
||||||
|
capitalizedIncomeAdjustmentTransactionIdRef.set(capitalizedIncomeAdjustmentTransactionId);
|
||||||
|
});
|
||||||
|
runAt("4 January 2024", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
executeInlineCOB(loanId);
|
||||||
|
|
||||||
verifyTransactions(loanId, //
|
verifyTransactions(loanId, //
|
||||||
transaction(100.0, "Disbursement", "01 January 2024"), //
|
transaction(100.0, "Disbursement", "01 January 2024"), //
|
||||||
@@ -297,7 +303,9 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
transaction(1.10, "Capitalized Income Amortization", "01 January 2024"), //
|
transaction(1.10, "Capitalized Income Amortization", "01 January 2024"), //
|
||||||
transaction(0.04, "Accrual", "02 January 2024"), //
|
transaction(0.04, "Accrual", "02 January 2024"), //
|
||||||
transaction(1.10, "Capitalized Income Amortization", "02 January 2024"), //
|
transaction(1.10, "Capitalized Income Amortization", "02 January 2024"), //
|
||||||
transaction(100.0, "Capitalized Income Adjustment", "03 January 2024") //
|
transaction(100.0, "Capitalized Income Adjustment", "03 January 2024"), //
|
||||||
|
transaction(0.04, "Accrual", "03 January 2024"), //
|
||||||
|
transaction(2.20, "Capitalized Income Amortization Adjustment", "03 January 2024") //
|
||||||
);
|
);
|
||||||
|
|
||||||
verifyJournalEntries(loanId, //
|
verifyJournalEntries(loanId, //
|
||||||
@@ -313,8 +321,11 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
journalEntry(1.10, feeIncomeAccount, "CREDIT"), //
|
journalEntry(1.10, feeIncomeAccount, "CREDIT"), //
|
||||||
journalEntry(99.92, loansReceivableAccount, "CREDIT"), //
|
journalEntry(99.92, loansReceivableAccount, "CREDIT"), //
|
||||||
journalEntry(0.08, interestReceivableAccount, "CREDIT"), //
|
journalEntry(0.08, interestReceivableAccount, "CREDIT"), //
|
||||||
|
journalEntry(100.0, deferredIncomeLiabilityAccount, "DEBIT"), //
|
||||||
|
journalEntry(0.04, interestReceivableAccount, "DEBIT"), //
|
||||||
|
journalEntry(0.04, interestIncomeAccount, "CREDIT"), //
|
||||||
journalEntry(2.20, feeIncomeAccount, "DEBIT"), //
|
journalEntry(2.20, feeIncomeAccount, "DEBIT"), //
|
||||||
journalEntry(97.80, deferredIncomeLiabilityAccount, "DEBIT") //
|
journalEntry(2.20, deferredIncomeLiabilityAccount, "CREDIT") //
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reverse-replay
|
// Reverse-replay
|
||||||
@@ -328,19 +339,16 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
verifyTRJournalEntries(replayedCapitalizedIncomeAdjustmentOpt.get().getId(), //
|
verifyTRJournalEntries(replayedCapitalizedIncomeAdjustmentOpt.get().getId(), //
|
||||||
journalEntry(99.98, loansReceivableAccount, "CREDIT"), //
|
journalEntry(99.98, loansReceivableAccount, "CREDIT"), //
|
||||||
journalEntry(0.02, interestReceivableAccount, "CREDIT"), //
|
journalEntry(0.02, interestReceivableAccount, "CREDIT"), //
|
||||||
journalEntry(2.20, feeIncomeAccount, "DEBIT"), //
|
journalEntry(100.0, deferredIncomeLiabilityAccount, "DEBIT") //
|
||||||
journalEntry(97.80, deferredIncomeLiabilityAccount, "DEBIT") //
|
|
||||||
);
|
);
|
||||||
|
|
||||||
verifyTRJournalEntries(capitalizedIncomeAdjustmentTransactionId, //
|
verifyTRJournalEntries(capitalizedIncomeAdjustmentTransactionIdRef.get(), //
|
||||||
journalEntry(99.92, loansReceivableAccount, "CREDIT"), //
|
journalEntry(99.92, loansReceivableAccount, "CREDIT"), //
|
||||||
journalEntry(0.08, interestReceivableAccount, "CREDIT"), //
|
journalEntry(0.08, interestReceivableAccount, "CREDIT"), //
|
||||||
journalEntry(2.20, feeIncomeAccount, "DEBIT"), //
|
journalEntry(100.0, deferredIncomeLiabilityAccount, "DEBIT"), //
|
||||||
journalEntry(97.80, deferredIncomeLiabilityAccount, "DEBIT"), //
|
|
||||||
journalEntry(99.92, loansReceivableAccount, "DEBIT"), //
|
journalEntry(99.92, loansReceivableAccount, "DEBIT"), //
|
||||||
journalEntry(0.08, interestReceivableAccount, "DEBIT"), //
|
journalEntry(0.08, interestReceivableAccount, "DEBIT"), //
|
||||||
journalEntry(2.20, feeIncomeAccount, "CREDIT"), //
|
journalEntry(100.0, deferredIncomeLiabilityAccount, "CREDIT") //
|
||||||
journalEntry(97.80, deferredIncomeLiabilityAccount, "CREDIT") //
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -486,12 +494,18 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
|
|
||||||
loanTransactionHelper.reverseLoanTransaction(loanId, capitalizedIncomeTransactionIdRef.get(), "3 January 2024");
|
loanTransactionHelper.reverseLoanTransaction(loanId, capitalizedIncomeTransactionIdRef.get(), "3 January 2024");
|
||||||
|
|
||||||
|
});
|
||||||
|
runAt("4 January 2024", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
executeInlineCOB(loanId);
|
||||||
|
|
||||||
verifyTransactions(loanId, //
|
verifyTransactions(loanId, //
|
||||||
transaction(100.0, "Disbursement", "01 January 2024"), //
|
transaction(100.0, "Disbursement", "01 January 2024"), //
|
||||||
transaction(50.0, "Capitalized Income", "01 January 2024"), //
|
transaction(50.0, "Capitalized Income", "01 January 2024"), //
|
||||||
transaction(0.55, "Capitalized Income Amortization", "01 January 2024"), //
|
transaction(0.55, "Capitalized Income Amortization", "01 January 2024"), //
|
||||||
transaction(0.03, "Accrual", "02 January 2024"), //
|
transaction(0.03, "Accrual", "02 January 2024"), //
|
||||||
transaction(0.55, "Capitalized Income Amortization", "02 January 2024"), //
|
transaction(0.55, "Capitalized Income Amortization", "02 January 2024"), //
|
||||||
|
transaction(0.01, "Accrual", "03 January 2024"), //
|
||||||
transaction(1.10, "Capitalized Income Amortization Adjustment", "03 January 2024") //
|
transaction(1.10, "Capitalized Income Amortization Adjustment", "03 January 2024") //
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -508,6 +522,8 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
|
|||||||
journalEntry(0.55, feeIncomeAccount, "CREDIT"), //
|
journalEntry(0.55, feeIncomeAccount, "CREDIT"), //
|
||||||
journalEntry(50, deferredIncomeLiabilityAccount, "DEBIT"), //
|
journalEntry(50, deferredIncomeLiabilityAccount, "DEBIT"), //
|
||||||
journalEntry(50, loansReceivableAccount, "CREDIT"), //
|
journalEntry(50, loansReceivableAccount, "CREDIT"), //
|
||||||
|
journalEntry(0.01, interestReceivableAccount, "DEBIT"), //
|
||||||
|
journalEntry(0.01, interestIncomeAccount, "CREDIT"), //
|
||||||
journalEntry(1.10, feeIncomeAccount, "DEBIT"), //
|
journalEntry(1.10, feeIncomeAccount, "DEBIT"), //
|
||||||
journalEntry(1.10, deferredIncomeLiabilityAccount, "CREDIT") //
|
journalEntry(1.10, deferredIncomeLiabilityAccount, "CREDIT") //
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user