Skip to content

Commit

Permalink
FINERACT-2148: Stop recalculating interest if the loan is charged-off
Browse files Browse the repository at this point in the history
  • Loading branch information
Jose Alberto Hernandez committed Jan 7, 2025
1 parent dfc1d3e commit b01a57e
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ private GetLoansTotal() {}

@Schema(example = "200.000000")
public Double amount;
@Schema(example = "100.000000")
public Double principalPortion;
@Schema(example = "80.000000")
public Double interestPortion;
@Schema(example = "20.000000")
public Double feeChargesPortion;
@Schema(example = "20.000000")
public Double penaltyChargesPortion;

public GetLoanCurrency currency;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2699,8 +2699,8 @@ public void processPostDisbursementTransactions() {
}

public LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO) {
if (!this.repaymentScheduleDetail().isEnableDownPayment()
&& (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa || isChargedOff())) {
if (!this.repaymentScheduleDetail().isEnableDownPayment() && (!this.repaymentScheduleDetail().isInterestRecalculationEnabled()
|| isNpa || isChargeOffOnDate(generatorDTO.getRecalculateFrom()))) {
return null;
}
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
Expand All @@ -2720,7 +2720,7 @@ public LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO genera
public OutstandingAmountsDTO fetchPrepaymentDetail(final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate onDate) {
OutstandingAmountsDTO outstandingAmounts;

if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled()) {
if (this.loanRepaymentScheduleDetail.isInterestRecalculationEnabled() && !isChargeOffOnDate(onDate)) {
final MathContext mc = MoneyHelper.getMathContext();

final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
Expand Down Expand Up @@ -3569,4 +3569,10 @@ public LoanRepaymentScheduleTransactionProcessor getTransactionProcessor() {
public boolean isProgressiveSchedule() {
return getLoanProductRelatedDetail().getLoanScheduleType() == PROGRESSIVE;
}

public boolean isChargeOffOnDate(final LocalDate onDate) {
final LoanTransaction chargeOffTransaction = findChargedOffTransaction();
return (chargeOffTransaction == null) ? false : (chargeOffTransaction.getDateOf().compareTo(onDate) <= 0);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public void regenerateRepaymentSchedule(final Loan loan, final ScheduleGenerator
}

public void recalculateScheduleFromLastTransaction(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !loan.isChargedOff()) {
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
} else {
regenerateRepaymentSchedule(loan, generatorDTO);
Expand All @@ -73,7 +73,7 @@ public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final Loa
* loanTransaction.getTransactionDate().isAfter(recalculateFrom)) { recalculateFrom =
* loanTransaction.getTransactionDate(); } } generatorDTO.setRecalculateFrom(recalculateFrom);
*/
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !loan.isChargedOff()) {
regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO);
} else {
regenerateRepaymentSchedule(loan, generatorDTO);
Expand All @@ -84,7 +84,7 @@ public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final Loa
public void regenerateRepaymentScheduleWithInterestRecalculation(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
final LocalDate lastTransactionDate = loan.getLastUserTransactionDate();
final LoanScheduleDTO loanSchedule = loan.getRecalculatedSchedule(generatorDTO);
if (loanSchedule == null) {
if (loanSchedule == null || loan.isChargedOff()) {
return;
}
// Either the installments got recalculated or the model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,7 @@ private void addInstallmentIfPenaltyAppliedAfterLastDueDate(Loan loan, LocalDate
}

public Loan runScheduleRecalculation(Loan loan, final LocalDate recalculateFrom) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && !loan.isChargedOff()) {
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loanScheduleService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
import org.apache.fineract.client.models.LoanProduct;
import org.apache.fineract.client.models.PaymentAllocationOrder;
import org.apache.fineract.client.models.PostClientsResponse;
Expand Down Expand Up @@ -82,6 +83,7 @@
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.organisation.StaffHelper;
import org.apache.fineract.integrationtests.common.system.CodeHelper;
import org.apache.fineract.integrationtests.useradministration.roles.RolesHelper;
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
Expand Down Expand Up @@ -5802,6 +5804,92 @@ public void uc152() {
});
}

// UC153: Validate Stop recalculating interest if the loan is charged-off
// 1. Create a Loan product with Adv. Pment. Alloc. and with Interest Recalculation enabled
// 2. Submit Loan, approve and Disburse
// 3. Apply Charge-Off
// 4. Prepay the loan to get the same interest amount after Charge-Off
@Test
public void uc153() {
AtomicLong createdLoanId = new AtomicLong();
runAt("01 January 2024", () -> {
String operationDate = "01 January 2024";
Long clientId = client.getClientId();
BigDecimal interestRatePerPeriod = BigDecimal.valueOf(7.0);

final Integer rescheduleStrategyMethod = 4; // Adjust last, unpaid period
PostLoanProductsRequest loanProduct = createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation(
(double) 80.0, rescheduleStrategyMethod);
final PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProduct);
assertNotNull(loanProductResponse);

PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 6);

applicationRequest = applicationRequest.numberOfRepayments(6)//
.loanTermFrequency(6)//
.loanTermFrequencyType(2)//
.interestRatePerPeriod(interestRatePerPeriod)//
.interestCalculationPeriodType(DAYS)//
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
.repaymentEvery(1)//
.repaymentFrequencyType(2)//
.maxOutstandingLoanBalance(BigDecimal.valueOf(10000.0))//
;//

PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);

loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()//
.approvedLoanAmount(BigDecimal.valueOf(100))//
.approvedOnDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));//

loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()//
.transactionAmount(BigDecimal.valueOf(100.0))//
.actualDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));//

GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 125.67, 0.0, 100.0, 0.0, null);
createdLoanId.set(loanResponse.getLoanId());
});

runAt("01 February 2024", () -> {

loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest()
.transactionDate("01 February 2024").dateFormat("dd MMMM yyyy").locale("en").transactionAmount(21.0));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
validateLoanSummaryBalances(loanDetails, 104.67, 21.0, 86.11, 13.89, null);
});

runAt("01 March 2024", () -> {
String randomText = Utils.randomStringGenerator("en", 5) + Utils.randomNumberGenerator(6)
+ Utils.randomStringGenerator("is", 5);
String transactionExternalId = UUID.randomUUID().toString();
Integer chargeOffReasonId = CodeHelper.createChargeOffCodeValue(requestSpec, responseSpec, randomText, 1);

loanTransactionHelper.chargeOffLoan(createdLoanId.get(),
new PostLoansLoanIdTransactionsRequest().transactionDate("01 March 2024").locale("en").dateFormat("dd MMMM yyyy")
.externalId(transactionExternalId).chargeOffReasonId((long) chargeOffReasonId));

// Loan Prepayment (before) Charge-Off transaction - With Interest Recalculation
GetLoansLoanIdTransactionsTemplateResponse transactionBefore = loanTransactionHelper
.retrieveTransactionTemplate(createdLoanId.get(), "prepayLoan", "dd MMMM yyyy", "15 February 2024", "en");
assertEquals(88.88, transactionBefore.getAmount());
assertEquals(86.11, transactionBefore.getPrincipalPortion());
assertEquals(2.77, transactionBefore.getInterestPortion());
assertEquals(0.00, transactionBefore.getFeeChargesPortion());
assertEquals(0.00, transactionBefore.getPenaltyChargesPortion());

// Loan Prepayment (after) Charge-Off transaction - WithOut Interest Recalculation
GetLoansLoanIdTransactionsTemplateResponse transactionAfter = loanTransactionHelper
.retrieveTransactionTemplate(createdLoanId.get(), "prepayLoan", "dd MMMM yyyy", "01 March 2024", "en");
assertEquals(104.67, transactionAfter.getAmount());
assertEquals(86.11, transactionAfter.getPrincipalPortion());
assertEquals(18.56, transactionAfter.getInterestPortion());
assertEquals(0.00, transactionAfter.getFeeChargesPortion());
assertEquals(0.00, transactionAfter.getPenaltyChargesPortion());
});

}

private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1920,6 +1920,11 @@ private String createChargebackPayload(final String transactionAmount, final Lon
return chargebackPayload;
}

public GetLoansLoanIdTransactionsTemplateResponse retrieveTransactionTemplate(Long loanId, String command, String dateFormat,
String transactionDate, String locale) {
return ok(fineract().loanTransactions.retrieveTransactionTemplate(loanId, command, dateFormat, transactionDate, locale));
}

public GetLoansLoanIdTransactionsTemplateResponse retrieveTransactionTemplate(String loanExternalIdStr, String command,
String dateFormat, String transactionDate, String locale) {
return ok(
Expand Down

0 comments on commit b01a57e

Please sign in to comment.