FINERACT-2333: Accrual handling in case of Charged-off loan

This commit is contained in:
mariiaKraievska
2025-03-03 16:30:46 +01:00
committed by Adam Saghy
parent 843b27926e
commit f0cc697c67
10 changed files with 426 additions and 15 deletions
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
import static java.math.BigDecimal.ZERO;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrueTransaction;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
@@ -60,6 +61,7 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
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.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
@@ -81,6 +83,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
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.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
@@ -109,9 +112,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME = "Advanced payment allocation strategy";
public final EMICalculator emiCalculator;
public final LoanRepositoryWrapper loanRepositoryWrapper;
public final InterestRefundService interestRefundService;
private final EMICalculator emiCalculator;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final InterestRefundService interestRefundService;
private final LoanTransactionRepository loanTransactionRepository;
private final ExternalIdFactory externalIdFactory;
@Override
public String getCode() {
@@ -1342,6 +1347,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
final BigDecimal newInterest = getInterestTillChargeOffForPeriod(loanTransaction.getLoan(), loanTransaction.getTransactionDate(),
transactionCtx);
createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction.getLoan(),
loanTransaction.getTransactionDate());
loanTransaction.resetDerivedComponents();
// determine how much is outstanding total and breakdown for principal, interest and charges
Money principalPortion = Money.zero(transactionCtx.getCurrency());
@@ -2267,4 +2277,71 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
processSingleTransaction(processTransaction, transactionCtx);
}
}
private BigDecimal getInterestTillChargeOffForPeriod(final Loan loan, final LocalDate chargeOffDate,
final TransactionCtx transactionCtx) {
BigDecimal interestTillChargeOff = BigDecimal.ZERO;
final MonetaryCurrency currency = loan.getCurrency();
final List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments().stream()
.filter(i -> !i.isAdditional()).toList();
for (LoanRepaymentScheduleInstallment installment : installments) {
final boolean isPastPeriod = !installment.getDueDate().isAfter(chargeOffDate);
final boolean isInPeriod = !installment.getFromDate().isAfter(chargeOffDate) && installment.getDueDate().isAfter(chargeOffDate);
BigDecimal interest = BigDecimal.ZERO;
if (isPastPeriod) {
interest = installment.getInterestCharged(currency).minus(installment.getCreditedInterest()).getAmount();
} else if (isInPeriod) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
&& loan.isInterestBearingAndInterestRecalculationEnabled()) {
interest = emiCalculator
.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(), installment.getDueDate(), chargeOffDate, true)
.getAmount();
} else {
final BigDecimal totalInterest = installment.getInterestOutstanding(currency).getAmount();
if (LoanChargeOffBehaviour.ZERO_INTEREST.equals(loan.getLoanProductRelatedDetail().getChargeOffBehaviour())
|| LoanChargeOffBehaviour.ACCELERATE_MATURITY
.equals(loan.getLoanProductRelatedDetail().getChargeOffBehaviour())) {
interest = totalInterest;
} else {
final long totalDaysInPeriod = ChronoUnit.DAYS.between(installment.getFromDate(), installment.getDueDate());
final long daysTillChargeOff = ChronoUnit.DAYS.between(installment.getFromDate(), chargeOffDate);
final MathContext mc = MoneyHelper.getMathContext();
interest = Money.of(currency, totalInterest.divide(BigDecimal.valueOf(totalDaysInPeriod), mc)
.multiply(BigDecimal.valueOf(daysTillChargeOff), mc), mc).getAmount();
}
}
}
interestTillChargeOff = interestTillChargeOff.add(interest);
}
return interestTillChargeOff;
}
private void createMissingAccrualTransactionDuringChargeOffIfNeeded(final BigDecimal newInterest, final Loan loan,
final LocalDate chargeOffDate) {
final List<LoanRepaymentScheduleInstallment> relevantInstallments = loan.getRepaymentScheduleInstallments().stream()
.filter(i -> !i.getFromDate().isAfter(chargeOffDate)).toList();
if (relevantInstallments.isEmpty()) {
return;
}
final BigDecimal sumOfAccrualsTillChargeOff = loan.getLoanTransactions().stream()
.filter(lt -> lt.isAccrual() && !lt.getTransactionDate().isAfter(chargeOffDate))
.map(lt -> Optional.ofNullable(lt.getInterestPortion()).orElse(BigDecimal.ZERO)).reduce(BigDecimal.ZERO, BigDecimal::add);
final BigDecimal missingAccrualAmount = newInterest.subtract(sumOfAccrualsTillChargeOff);
if (missingAccrualAmount.compareTo(BigDecimal.ZERO) > 0) {
final LoanTransaction newAccrualTransaction = accrueTransaction(loan, loan.getOffice(), chargeOffDate, missingAccrualAmount,
missingAccrualAmount, ZERO, ZERO, externalIdFactory.create());
loan.addLoanTransaction(newAccrualTransaction);
loanTransactionRepository.saveAndFlush(newAccrualTransaction);
}
}
}
@@ -111,7 +111,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
@BeforeEach
public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null);
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null, null, null);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);