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

This commit is contained in:
Jose Alberto Hernandez
2025-01-08 12:07:23 +01:00
committed by Adam Saghy
parent e1d043bb2d
commit 4f1184f20b
6 changed files with 111 additions and 4 deletions
@@ -79,6 +79,14 @@ final class LoanTransactionsApiResourceSwagger {
@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;
}
@@ -2722,7 +2722,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
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();
@@ -3571,4 +3571,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
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);
}
}
@@ -54,7 +54,7 @@ public class LoanScheduleService {
}
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);
@@ -73,7 +73,7 @@ public class LoanScheduleService {
* 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);
@@ -1154,7 +1154,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
}
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
@@ -52,6 +52,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData;
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;
@@ -82,6 +83,7 @@ import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
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;
@@ -5802,6 +5804,92 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
});
}
// 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 ---------------------------------------");
@@ -1920,6 +1920,11 @@ public class LoanTransactionHelper extends IntegrationTest {
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(