Skip to content

Commit

Permalink
FINERACT-2148: Update instalments interest with zero after charge off…
Browse files Browse the repository at this point in the history
… with interest recalculation enabled
oleksii-novikov-onix authored and adamsaghy committed Dec 16, 2024
1 parent 136bb00 commit 51433ff
Showing 8 changed files with 436 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -73,6 +73,7 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_WHOLE_TERM, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_PRECLOSE_LAST_INSTALLMENT_STRATEGY, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION, //
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR, //
;

@Override
Original file line number Diff line number Diff line change
@@ -1088,6 +1088,23 @@ public void initialize() throws Exception {
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_CUSTOM_PAYMENT_ALLOCATION_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE,
responseLoanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDaily36030MultiDisburse);

// LP2 + interest recalculation + zero-interest chargeOff behaviour + progressive loan schedule + horizontal
// (LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR)
final String name51 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR
.getName();

final PostLoanProductsRequest loanProductsRequestAdvCustomZeroInterestChargeOffBehaviourProgressiveLoanSchedule = loanProductsRequestFactory
.defaultLoanProductsRequestLP2InterestDailyRecalculation()//
.name(name51)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT")))
.chargeOffBehaviour("ZERO_INTEREST");//
final Response<PostLoanProductsResponse> responseLoanProductsRequestAdvCustomZeroInterestChargeOffBehaviourProgressiveLoanSchedule = loanProductsApi
.createLoanProduct(loanProductsRequestAdvCustomZeroInterestChargeOffBehaviourProgressiveLoanSchedule).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR,
responseLoanProductsRequestAdvCustomZeroInterestChargeOffBehaviourProgressiveLoanSchedule);
}

public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
package org.apache.fineract.test.stepdef.loan;

import static org.apache.fineract.test.data.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION;
import static org.apache.fineract.test.data.loanproduct.DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertNotNull;
@@ -2766,6 +2767,34 @@ public void createFullyCustomizedLoanWithInterestRateFrequency(final List<String
eventCheckHelper.createLoanEventCheck(response);
}

@When("Admin creates a new zero charge-off Loan with date: {string}")
public void createLoanWithInterestRecalculationAndZeroChargeOffBehaviour(final String date) throws IOException {
final Response<PostClientsResponse> clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
final Long clientId = clientResponse.body().getClientId();

final DefaultLoanProduct product = DefaultLoanProduct
.valueOf(LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR.getName());
final Long loanProductId = loanProductResolver.resolve(product);

final PostLoansRequest loansRequest = loanRequestFactory.defaultLoansRequest(clientId).productId(loanProductId)
.principal(new BigDecimal(100)).numberOfRepayments(6).submittedOnDate(date).expectedDisbursementDate(date)
.loanTermFrequency(6)//
.loanTermFrequencyType(LoanTermFrequencyType.MONTHS.value)//
.repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.value)//
.interestRateFrequencyType(3)//
.interestRatePerPeriod(new BigDecimal(7))//
.interestType(InterestType.DECLINING_BALANCE.value)//
.interestCalculationPeriodType(InterestCalculationPeriodTime.DAILY.value)//
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.value);

final Response<PostLoansResponse> response = loansApi.calculateLoanScheduleOrSubmitLoanApplication(loansRequest, "").execute();
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
ErrorHelper.checkSuccessfulApiCall(response);

eventCheckHelper.createLoanEventCheck(response);
}

private void performLoanDisbursementAndVerifyStatus(final long loanId, final PostLoansLoanIdRequest disburseRequest)
throws IOException {
final Response<PostLoansLoanIdResponse> loanDisburseResponse = loansApi.stateTransitions(loanId, disburseRequest, "disburse")
Original file line number Diff line number Diff line change
@@ -104,6 +104,7 @@ public abstract class TestContextKey {
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_ADVANCED_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL = "loanProductCreateResponseLP1ProgressiveLoanScheduleHorizontal";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_PRECLOSE_WHOLE_TERM = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationSameAsRepTillPreCloseWholeTerm";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_INTEREST_RECALCULATION = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmiActualActualInterestRefundFInterestRecalculation";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_BEHAVIOUR = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationZeroInterestChargeOffBehaviour";
public static final String CHARGE_FOR_LOAN_PERCENT_LATE_CREATE_RESPONSE = "ChargeForLoanPercentLateCreateResponse";
public static final String CHARGE_FOR_LOAN_PERCENT_LATE_AMOUNT_PLUS_INTEREST_CREATE_RESPONSE = "ChargeForLoanPercentLateAmountPlusInterestCreateResponse";
public static final String CHARGE_FOR_LOAN_PERCENT_PROCESSING_CREATE_RESPONSE = "ChargeForLoanPercentProcessingCreateResponse";

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -255,6 +255,7 @@ public void processLatestTransaction(final LoanTransaction loanTransaction, fina
case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx);
default -> {
Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, ctx.getCurrency(), ctx.getInstallments(),
ctx.getCharges(), null, false);
@@ -394,7 +395,7 @@ private void recalculateChargeOffTransaction(ChangedTransactionDetail changedTra
principalPortion = principalPortion.plus(currentInstallment.getPrincipalOutstanding(currency));
interestPortion = interestPortion.plus(currentInstallment.getInterestOutstanding(currency));
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesOutstanding(currency));
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency));
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesOutstanding(currency));
}
}

@@ -786,6 +787,11 @@ protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx
processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
}

private void handleChargeOff(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
recalculateChargeOffTransaction(transactionCtx.getChangedTransactionDetail(), loanTransaction, transactionCtx.getCurrency(),
transactionCtx.getInstallments());
}

protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaidAmountHolder) {
processCreditTransaction(loanTransaction, overpaidAmountHolder, currency, installments);
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -68,6 +69,7 @@
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
@@ -1168,7 +1170,14 @@ private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTra
}
}

private void handleChargeOff(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
private void handleChargeOff(final LoanTransaction loanTransaction, final TransactionCtx transactionCtx) {
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx) {
if (LoanChargeOffBehaviour.ZERO_INTEREST.equals(loanTransaction.getLoan().getLoanProductRelatedDetail().getChargeOffBehaviour())
&& !loanTransaction.isReversed()) {
handleZeroInterestChargeOff(loanTransaction, progressiveTransactionCtx);
}
}

loanTransaction.resetDerivedComponents();
// determine how much is outstanding total and breakdown for principal, interest and charges
Money principalPortion = Money.zero(transactionCtx.getCurrency());
@@ -1188,6 +1197,58 @@ private void handleChargeOff(LoanTransaction loanTransaction, TransactionCtx tra
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
}

private void handleZeroInterestChargeOff(final LoanTransaction loanTransaction,
final ProgressiveTransactionCtx progressiveTransactionCtx) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final List<LoanRepaymentScheduleInstallment> installments = progressiveTransactionCtx.getInstallments();

if (!installments.isEmpty()) {
if (loanTransaction.getLoan().isInterestRecalculationEnabled()) {
installments.stream().filter(installment -> !installment.getFromDate().isAfter(transactionDate)
&& installment.getDueDate().isAfter(transactionDate)).forEach(installment -> {
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
installment.getDueDate(), transactionDate).getAmount();
final BigDecimal interestRemoved = installment.getInterestCharged().subtract(newInterest);
installment.updatePrincipal(MathUtil.nullToZero(installment.getPrincipal()).add(interestRemoved));
installment.updateInterestCharged(newInterest);
});
} else {
calculatePartialPeriodInterest(progressiveTransactionCtx, transactionDate);
}

installments.stream().filter(installment -> installment.getFromDate().isAfter(transactionDate)).forEach(installment -> {
installment.updatePrincipal(MathUtil.nullToZero(installment.getPrincipal()).add(installment.getInterestCharged()));
installment.updateInterestCharged(BigDecimal.ZERO);
});

final BigDecimal totalInstallmentsPrincipal = installments.stream().map(LoanRepaymentScheduleInstallment::getPrincipal)
.reduce(ZERO, BigDecimal::add);
final Money amountToEditLastInstallment = loanTransaction.getLoan().getPrincipal().minus(totalInstallmentsPrincipal);
final int lastInstallmentIndex = installments.size() - 1;
installments.get(lastInstallmentIndex)
.updatePrincipal(installments.get(lastInstallmentIndex).getPrincipal().add(amountToEditLastInstallment.getAmount()));
}
}

private void calculatePartialPeriodInterest(final ProgressiveTransactionCtx progressiveTransactionCtx, final LocalDate chargeOffDate) {
progressiveTransactionCtx.getInstallments().stream()
.filter(installment -> !installment.getFromDate().isAfter(chargeOffDate) && installment.getDueDate().isAfter(chargeOffDate))
.forEach(installment -> {
final BigDecimal totalInterest = installment.getInterestOutstanding(progressiveTransactionCtx.getCurrency())
.getAmount();
final long totalDaysInPeriod = ChronoUnit.DAYS.between(installment.getFromDate(), installment.getDueDate());
final long daysTillChargeOff = ChronoUnit.DAYS.between(installment.getFromDate(), chargeOffDate);

final BigDecimal interestTillChargeOff = totalInterest
.divide(BigDecimal.valueOf(totalDaysInPeriod), MoneyHelper.getMathContext())
.multiply(BigDecimal.valueOf(daysTillChargeOff));

final BigDecimal interestRemoved = totalInterest.subtract(interestTillChargeOff);
installment.updatePrincipal(MathUtil.nullToZero(installment.getPrincipal()).add(interestRemoved));
installment.updateInterestCharged(interestTillChargeOff);
});
}

private void handleChargePayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
Money zero = Money.zero(transactionCtx.getCurrency());
Money feeChargesPortion = zero;
Original file line number Diff line number Diff line change
@@ -3207,10 +3207,9 @@ public CommandProcessingResult chargeOff(JsonCommand command) {
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds();

LoanTransaction chargeOffTransaction = LoanTransaction.chargeOff(loan, transactionDate, txnExternalId);
final LoanTransaction chargeOffTransaction = LoanTransaction.chargeOff(loan, transactionDate, txnExternalId);

if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()
&& DateUtils.isBefore(loan.getInterestRecalculatedOn(), DateUtils.getBusinessLocalDate())) {
if (loan.isInterestBearing() && loan.isInterestRecalculationEnabled()) {
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
loan.addLoanTransaction(chargeOffTransaction);
@@ -3225,6 +3224,10 @@ public CommandProcessingResult chargeOff(JsonCommand command) {
}
} else {
loan.addLoanTransaction(chargeOffTransaction);
loan.getTransactionProcessor().processLatestTransaction(chargeOffTransaction,
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields();
}
loanTransactionRepository.saveAndFlush(chargeOffTransaction);

@@ -3292,6 +3295,9 @@ public CommandProcessingResult undoChargeOff(JsonCommand command) {
saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoChargeOffBusinessEvent(chargedOffTransaction));

loan.reprocessTransactions();

return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //

0 comments on commit 51433ff

Please sign in to comment.