FINERACT-2311: Buy down fee - daily amortization
This commit is contained in:
+159
@@ -153,6 +153,14 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
if (transactionType.isBuyDownFeeAdjustment()) {
|
||||
createJournalEntriesForBuyDownFeeAdjustment(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
// Handle Buy Down Fee Amortization
|
||||
if (transactionType.isBuyDownFeeAmortization()) {
|
||||
createJournalEntriesForBuyDownFeeAmortization(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
// Handle Buy Down Fee Amortization Adjustment
|
||||
if (transactionType.isBuyDownFeeAmortizationAdjustment()) {
|
||||
createJournalEntriesForBuyDownFeeAmortizationAdjustment(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +470,157 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForBuyDownFeeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||
final Office office) {
|
||||
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
|
||||
if (isMarkedAsChargeOff) {
|
||||
createJournalEntriesForChargeOffLoanBuyDownFeeAmortization(loanDTO, loanTransactionDTO, office);
|
||||
} else {
|
||||
createJournalEntriesForLoanBuyDownFeeAmortization(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForLoanBuyDownFeeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||
final Office office) {
|
||||
// loan properties
|
||||
final Long loanProductId = loanDTO.getLoanProductId();
|
||||
final Long loanId = loanDTO.getLoanId();
|
||||
final String currencyCode = loanDTO.getCurrencyCode();
|
||||
final boolean isLoanWrittenOff = loanDTO.isMarkedAsWrittenOff();
|
||||
|
||||
// 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();
|
||||
|
||||
// interest payment
|
||||
final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ? AccrualAccountsForLoan.LOSSES_WRITTEN_OFF
|
||||
: AccrualAccountsForLoan.INCOME_FROM_BUY_DOWN;
|
||||
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 createJournalEntriesForChargeOffLoanBuyDownFeeAmortization(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 createJournalEntriesForBuyDownFeeAmortizationAdjustment(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||
final Office office) {
|
||||
GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
|
||||
if (MathUtil.isGreaterThanZero(loanTransactionDTO.getAmount())) {
|
||||
populateCreditDebitMaps(loanDTO.getLoanProductId(), loanTransactionDTO.getAmount(), loanTransactionDTO.getPaymentTypeId(),
|
||||
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), AccrualAccountsForLoan.INCOME_FROM_BUY_DOWN.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, loanDTO.getCurrencyCode(), loanDTO.getLoanId(),
|
||||
loanTransactionDTO.getTransactionId(), loanTransactionDTO.getTransactionDate(), 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, loanDTO.getCurrencyCode(), loanDTO.getLoanId(),
|
||||
loanTransactionDTO.getTransactionId(), loanTransactionDTO.getTransactionDate(), debitEntry.getValue(), glAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
|
||||
Office office) {
|
||||
final Long loanProductId = loanDTO.getLoanProductId();
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 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.cob.loan;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBuyDownFeeAmortizationProcessingService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class BuyDownFeeAmortizationBusinessStep implements LoanCOBBusinessStep {
|
||||
|
||||
private final LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public Loan execute(Loan loan) {
|
||||
if (!loan.getLoanProductRelatedDetail().isEnableBuyDownFee()) {
|
||||
return loan;
|
||||
}
|
||||
|
||||
LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||
|
||||
loanBuyDownFeeAmortizationProcessingService.processBuyDownFeeAmortizationTillDate(loan, businessDate, true);
|
||||
|
||||
return loan;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEnumStyledName() {
|
||||
return "BUY_DOWN_FEE_AMORTIZATION";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHumanReadableName() {
|
||||
return "Buy Down Fee amortization";
|
||||
}
|
||||
}
|
||||
+2
@@ -537,6 +537,8 @@ public class LoanTransactionsApiResource {
|
||||
case capitalizedIncomeAdjustment -> LoanTransactionType.CAPITALIZED_INCOME_ADJUSTMENT;
|
||||
case contractTermination -> LoanTransactionType.CONTRACT_TERMINATION;
|
||||
case capitalizedIncomeAmortizationAdjustment -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION_ADJUSTMENT;
|
||||
case buyDownFeeAmortization -> LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION;
|
||||
case buyDownFeeAmortizationAdjustment -> LoanTransactionType.BUY_DOWN_FEE_AMORTIZATION_ADJUSTMENT;
|
||||
default ->
|
||||
throw new InvalidLoanTransactionTypeException("transaction", transactionTypeParam.name(), "Unknown transaction type");
|
||||
};
|
||||
|
||||
+8
@@ -735,6 +735,14 @@ final class LoansApiResourceSwagger {
|
||||
public boolean capitalizedIncomeAdjustment;
|
||||
@Schema(example = "false")
|
||||
public boolean contractTermination;
|
||||
@Schema(example = "false")
|
||||
public boolean buyDownFee;
|
||||
@Schema(example = "false")
|
||||
public boolean buyDownFeeAdjustment;
|
||||
@Schema(example = "false")
|
||||
public boolean buyDownFeeAmortization;
|
||||
@Schema(example = "false")
|
||||
public boolean buyDownFeeAmortizationAdjustment;
|
||||
}
|
||||
|
||||
static final class GetLoansLoanIdPaymentDetailData {
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.transaction.LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent;
|
||||
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.LoanBuyDownFeeBalance;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanBuyDownFeesBalanceRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.util.BuyDownFeeAmortizationUtil;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LoanBuyDownFeeAmortizationProcessingServiceImpl implements LoanBuyDownFeeAmortizationProcessingService {
|
||||
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanJournalEntryPoster journalEntryPoster;
|
||||
private final ExternalIdFactory externalIdFactory;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void processBuyDownFeeAmortizationTillDate(@NonNull Loan loan, @NonNull LocalDate tillDate, boolean addJournal) {
|
||||
final List<Long> existingTransactionIds = loanTransactionRepository.findTransactionIdsByLoan(loan);
|
||||
final List<Long> existingReversedTransactionIds = loanTransactionRepository.findReversedTransactionIdsByLoan(loan);
|
||||
|
||||
List<LoanBuyDownFeeBalance> balances = loanBuyDownFeesBalanceRepository.findAllByLoanId(loan.getId());
|
||||
|
||||
LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate()
|
||||
: getFinalBuyDownFeeAmortizationTransactionDate(loan);
|
||||
LocalDate tillDatePlusOne = tillDate.plusDays(1);
|
||||
if (tillDatePlusOne.isAfter(maturityDate)) {
|
||||
tillDatePlusOne = maturityDate;
|
||||
}
|
||||
|
||||
Money totalAmortization = Money.zero(loan.getCurrency());
|
||||
for (LoanBuyDownFeeBalance balance : balances) {
|
||||
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
|
||||
Money amortizationTillDate = BuyDownFeeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments, maturityDate,
|
||||
loan.getLoanProductRelatedDetail().getBuyDownFeeStrategy(), tillDatePlusOne, loan.getCurrency());
|
||||
totalAmortization = totalAmortization.add(amortizationTillDate);
|
||||
|
||||
balance.setUnrecognizedAmount(balance.getAmount().subtract(MathUtil.nullToZero(balance.getAmountAdjustment()))
|
||||
.subtract(amortizationTillDate.getAmount()));
|
||||
}
|
||||
|
||||
loanBuyDownFeesBalanceRepository.saveAll(balances);
|
||||
|
||||
BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountBuyDownFee(loan);
|
||||
BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized);
|
||||
|
||||
if (!MathUtil.isZero(totalAmortizationAmount)) {
|
||||
LoanTransaction transaction = MathUtil.isGreaterThanZero(totalAmortizationAmount)
|
||||
? LoanTransaction.buyDownFeeAmortization(loan, loan.getOffice(), tillDate, totalAmortizationAmount,
|
||||
externalIdFactory.create())
|
||||
: LoanTransaction.buyDownFeeAmortizationAdjustment(loan,
|
||||
Money.of(loan.getCurrency(), MathUtil.negate(totalAmortizationAmount)), tillDate, externalIdFactory.create());
|
||||
loan.addLoanTransaction(transaction);
|
||||
|
||||
transaction = loanTransactionRepository.save(transaction);
|
||||
loanTransactionRepository.flush();
|
||||
|
||||
if (addJournal) {
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
|
||||
BusinessEvent<?> event = MathUtil.isGreaterThanZero(totalAmortizationAmount)
|
||||
? new LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent(transaction)
|
||||
: new LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent(transaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate getFinalBuyDownFeeAmortizationTransactionDate(final Loan loan) {
|
||||
return switch (loan.getStatus()) {
|
||||
case CLOSED_OBLIGATIONS_MET -> loan.getClosedOnDate();
|
||||
case OVERPAID -> loan.getOverpaidOnDate();
|
||||
case CLOSED_WRITTEN_OFF -> loan.getWrittenOffOnDate();
|
||||
default -> throw new IllegalStateException("Unexpected value: " + loan.getStatus());
|
||||
};
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -125,7 +125,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
|
||||
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
||||
for (LoanCapitalizedIncomeBalance balance : balances) {
|
||||
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustmentsForCapitalizedIncome(balance.getLoanTransaction());
|
||||
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
|
||||
LocalDate maturityDate = loan.getMaturityDate() != null ? loan.getMaturityDate() : transactionDate;
|
||||
final Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
||||
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), maturityDate, loan.getCurrency());
|
||||
@@ -136,7 +136,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmount(loan);
|
||||
BigDecimal amortizedAmount = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan);
|
||||
BigDecimal totalUnrecognizedAmount = totalAmortizationAmount.subtract(amortizedAmount);
|
||||
if (MathUtil.isZero(totalUnrecognizedAmount)) {
|
||||
return Optional.empty();
|
||||
@@ -203,7 +203,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
|
||||
Money totalAmortization = Money.zero(loan.getCurrency());
|
||||
for (LoanCapitalizedIncomeBalance balance : balances) {
|
||||
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustmentsForCapitalizedIncome(balance.getLoanTransaction());
|
||||
List<LoanTransaction> adjustments = loanTransactionRepository.findAdjustments(balance.getLoanTransaction());
|
||||
Money amortizationTillDate = CapitalizedIncomeAmortizationUtil.calculateTotalAmortizationTillDate(balance, adjustments,
|
||||
maturityDate, loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), tillDatePlusOne, loan.getCurrency());
|
||||
totalAmortization = totalAmortization.add(amortizationTillDate);
|
||||
@@ -214,7 +214,7 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
|
||||
loanCapitalizedIncomeBalanceRepository.saveAll(balances);
|
||||
|
||||
BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmount(loan);
|
||||
BigDecimal totalAmortized = loanTransactionRepository.getAmortizedAmountCapitalizedIncome(loan);
|
||||
BigDecimal totalAmortizationAmount = totalAmortization.getAmount().subtract(totalAmortized);
|
||||
|
||||
if (!MathUtil.isZero(totalAmortizationAmount)) {
|
||||
|
||||
+24
@@ -123,9 +123,12 @@ 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.LoanBuyDownFeeAmortizationProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBuyDownFeeAmortizationProcessingServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
|
||||
@@ -549,4 +552,25 @@ public class LoanAccountConfiguration {
|
||||
loanCapitalizedIncomeAmortizationProcessingService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanCapitalizedIncomeAmortizationProcessingService.class)
|
||||
public LoanCapitalizedIncomeAmortizationProcessingService loanCapitalizedIncomeAmortizationProcessingService(
|
||||
final ConfigurationDomainService configurationDomainService, final LoanTransactionRepository loanTransactionRepository,
|
||||
final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository,
|
||||
final BusinessEventNotifierService businessEventNotifierService, final LoanJournalEntryPoster journalEntryPoster,
|
||||
final ExternalIdFactory externalIdFactory) {
|
||||
return new LoanCapitalizedIncomeAmortizationProcessingServiceImpl(configurationDomainService, loanTransactionRepository,
|
||||
loanCapitalizedIncomeBalanceRepository, businessEventNotifierService, journalEntryPoster, externalIdFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(LoanBuyDownFeeAmortizationProcessingService.class)
|
||||
public LoanBuyDownFeeAmortizationProcessingService loanBuyDownFeeAmortizationProcessingService(
|
||||
final LoanTransactionRepository loanTransactionRepository,
|
||||
final LoanBuyDownFeesBalanceRepository loanBuyDownFeesBalanceRepository,
|
||||
final BusinessEventNotifierService businessEventNotifierService, final LoanJournalEntryPoster journalEntryPoster,
|
||||
final ExternalIdFactory externalIdFactory) {
|
||||
return new LoanBuyDownFeeAmortizationProcessingServiceImpl(loanTransactionRepository, loanBuyDownFeesBalanceRepository,
|
||||
businessEventNotifierService, journalEntryPoster, externalIdFactory);
|
||||
}
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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.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.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeBalance;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanBuyDownFeeStrategy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
public final class BuyDownFeeAmortizationUtil {
|
||||
|
||||
private BuyDownFeeAmortizationUtil() {}
|
||||
|
||||
public static Money calculateTotalAmortizationTillDate(final LoanBuyDownFeeBalance buyDownFeeBalance,
|
||||
final List<LoanTransaction> adjustmentTransactions, final LocalDate maturityDate,
|
||||
final LoanBuyDownFeeStrategy buyDownFeeStrategy, final LocalDate tillDate, final MonetaryCurrency currency) {
|
||||
return switch (buyDownFeeStrategy) {
|
||||
case EQUAL_AMORTIZATION -> calculateTotalAmortizationTillDateEqualAmortization(buyDownFeeBalance, adjustmentTransactions,
|
||||
maturityDate, tillDate, currency);
|
||||
};
|
||||
}
|
||||
|
||||
private static Money calculateTotalAmortizationTillDateEqualAmortization(LoanBuyDownFeeBalance 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));
|
||||
}
|
||||
}
|
||||
@@ -208,4 +208,5 @@
|
||||
<include file="parts/0187_add_Loan_modified_and_withdrawn_event_configuration.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0188_create_loan_buy_down_fee_balance.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0189_add_loan_buydown_fee_event.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?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.1.xsd">
|
||||
<changeSet id="1" author="fineract">
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="r_enum_value">
|
||||
<column name="enum_name" value="transaction_type_enum"/>
|
||||
<column name="enum_id" valueNumeric="42"/>
|
||||
<column name="enum_message_property" value="Buy Down Fee Amortization"/>
|
||||
<column name="enum_value" value="Buy Down Fee Amortization"/>
|
||||
<column name="enum_type" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="r_enum_value">
|
||||
<column name="enum_name" value="transaction_type_enum"/>
|
||||
<column name="enum_id" valueNumeric="43"/>
|
||||
<column name="enum_message_property" value="Buy Down Fee Amortization Adjustment"/>
|
||||
<column name="enum_value" value="Buy Down Fee Amortization Adjustment"/>
|
||||
<column name="enum_type" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+6
-2
@@ -110,7 +110,9 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
|
||||
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
|
||||
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
|
||||
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
|
||||
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
|
||||
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
|
||||
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
@@ -202,7 +204,9 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", "LoanTransactionContractTerminationPostBusinessEvent",
|
||||
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
|
||||
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent", "LoanUndoContractTerminationBusinessEvent",
|
||||
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent");
|
||||
"LoanBuyDownFeeTransactionCreatedBusinessEvent", "LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
|
||||
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
|
||||
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
|
||||
Reference in New Issue
Block a user