Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FINERACT-2148: Stop recalculating interest if the loan is charged-off #4234

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2722,7 +2722,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 @@ -3571,4 +3571,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()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you call loan.isChargedOff(), it simply checks whether the loan has been charged off. As a result, you might skip the recalculation for installments that have dates earlier than the charge-off transaction. As far as I understand, you need to stop recalculating interest only AFTER the date of the charge-off transaction. Perhaps my PR could help you: #4236

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solved!

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 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
Loading