Skip to content

Commit fa7d2fb

Browse files
author
Jose Alberto Hernandez
committed
FINERACT-2326: Loan contract termination same disbursement date
1 parent fe4c970 commit fa7d2fb

File tree

10 files changed

+133
-13
lines changed

10 files changed

+133
-13
lines changed

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ public enum DefaultLoanProduct implements LoanProduct {
146146
LP2_ADV_PYMNT_ZERO_INTEREST_CHARGE_OFF_DELINQUENT_REASON_INTEREST_RECALC_CAPITALIZED_INCOME, //
147147
LP2_ADV_PYMNT_360_30_INTEREST_RECALCULATION_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY, //
148148
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION, //
149+
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION, //
149150
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_OVER_APPLIED_PERCENTAGE_CAPITALIZED_INCOME, //
150151
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_OVER_APPLIED_FLAT_CAPITALIZED_INCOME, //
151152
LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_APPROVED_OVER_APPLIED_PERCENTAGE_CAPITALIZED_INCOME, //

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4058,6 +4058,39 @@ public void initialize() throws Exception {
40584058
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY,
40594059
responseLoanProductsResponseAdvCustomPaymentAllocationProgressiveLoanInterestDailyEmi36030InterestRecalculationDaily);
40604060

4061+
// LP2 with progressive loan schedule + horizontal + interest EMI + 360/30 + multidisbursement +
4062+
// contract termination with interest recognition
4063+
// (LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION)
4064+
final String name148 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION
4065+
.getName();
4066+
4067+
final PostLoanProductsRequest loanProductsRequestAdvCustomContractTerminationProgressiveLoanScheduleIntRecalcRecog = loanProductsRequestFactory
4068+
.defaultLoanProductsRequestLP2InterestDailyRecalculation()//
4069+
.interestRecognitionOnDisbursementDate(true) //
4070+
.name(name148)//
4071+
.paymentAllocation(List.of(//
4072+
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT",
4073+
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PENALTY, //
4074+
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_FEE, //
4075+
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_INTEREST, //
4076+
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PRINCIPAL, //
4077+
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PENALTY, //
4078+
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_FEE, //
4079+
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PRINCIPAL, //
4080+
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_INTEREST, //
4081+
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PENALTY, //
4082+
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_FEE, //
4083+
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PRINCIPAL, //
4084+
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_INTEREST), //
4085+
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
4086+
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
4087+
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
4088+
final Response<PostLoanProductsResponse> responseLoanProductsRequestAdvCustomContractTerminationProgressiveLoanScheduleIntRecalcRecog = loanProductsApi
4089+
.createLoanProduct(loanProductsRequestAdvCustomContractTerminationProgressiveLoanScheduleIntRecalcRecog).execute();
4090+
TestContext.INSTANCE.set(
4091+
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION,
4092+
responseLoanProductsRequestAdvCustomContractTerminationProgressiveLoanScheduleIntRecalcRecog);
4093+
40614094
// (LP1_WITH_OVERRIDES) - Loan product with all attribute overrides ENABLED
40624095
final String nameWithOverrides = DefaultLoanProduct.LP1_WITH_OVERRIDES.getName();
40634096
final PostLoanProductsRequest loanProductsRequestWithOverrides = loanProductsRequestFactory.defaultLoanProductsRequestLP1() //

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ public abstract class TestContextKey {
258258
public static final String LOAN_INTEREST_REFUND_RESPONSE = "loanInterestRefundResponse";
259259
public static final String INTEREST_PAUSE_VARIATION_ID = "interestPauseVariationId";
260260
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationContractTermination";
261+
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationContractTerminationIntRecognition";
261262
public static final String LOAN_CONTRACT_TERMINATION_RESPONSE = "loanContractTerminationResponse";
262263
public static final String LOAN_UNDO_CONTRACT_TERMINATION_RESPONSE = "loanUndoContractTerminationResponse";
263264
public static final String LOAN_BUY_DOWN_FEE_RESPONSE = "loanBuyDownFeeResponse";

fineract-e2e-tests-runner/src/test/resources/features/LoanContractTermination.feature

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,3 +1317,47 @@ Feature: Contract Termination
13171317
| 31 March 2024 | Accrual Adjustment | 0.15 | 0.0 | 0.15 | 0.0 | 0.0 | 0.0 | false | false |
13181318
| 31 March 2024 | Contract Termination | 57.37 | 57.05 | 0.32 | 0.0 | 0.0 | 0.0 | true | true |
13191319
And Global configuration "is-principal-compounding-disabled-for-overdue-loans" is disabled
1320+
1321+
@TestRailId:C4133
1322+
Scenario: Contract termination on disbursement date
1323+
When Admin sets the business date to "01 January 2025"
1324+
And Admin creates a client with random data
1325+
And Admin creates a fully customized loan with the following data:
1326+
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
1327+
| LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION | 01 January 2025 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
1328+
And Admin successfully approves the loan on "01 January 2025" with "100" amount and expected disbursement date on "01 January 2025"
1329+
And Admin successfully disburse the loan on "01 January 2025" with "100" EUR transaction amount
1330+
And Admin successfully terminates loan contract
1331+
Then Loan Repayment schedule has 1 periods, with the following data for periods:
1332+
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
1333+
| | | 01 January 2025 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
1334+
| 1 | 0 | 01 January 2025 | | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
1335+
And Loan Repayment schedule has the following data in Total row:
1336+
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
1337+
| 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
1338+
And Loan Transactions tab has the following data:
1339+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
1340+
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
1341+
| 01 January 2025 | Contract Termination | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false |
1342+
1343+
@TestRailId:C4134
1344+
Scenario: Contract termination on disbursement date with interest recognition
1345+
When Admin sets the business date to "01 January 2025"
1346+
And Admin creates a client with random data
1347+
And Admin creates a fully customized loan with the following data:
1348+
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
1349+
| LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_CONTRACT_TERMINATION_INT_RECOGNITION | 01 January 2025 | 100 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
1350+
And Admin successfully approves the loan on "01 January 2025" with "100" amount and expected disbursement date on "01 January 2025"
1351+
And Admin successfully disburse the loan on "01 January 2025" with "100" EUR transaction amount
1352+
And Admin successfully terminates loan contract
1353+
Then Loan Repayment schedule has 1 periods, with the following data for periods:
1354+
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
1355+
| | | 01 January 2025 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
1356+
| 1 | 0 | 01 January 2025 | | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
1357+
And Loan Repayment schedule has the following data in Total row:
1358+
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
1359+
| 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
1360+
And Loan Transactions tab has the following data:
1361+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
1362+
| 01 January 2025 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false | false |
1363+
| 01 January 2025 | Contract Termination | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false |

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,10 +1410,17 @@ public void addLoanRepaymentScheduleInstallment(final LoanRepaymentScheduleInsta
14101410
* @param date
14111411
* @return a schedule installment is related to the provided date
14121412
**/
1413-
public LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(LocalDate date) {
1414-
return getRepaymentScheduleInstallment(
1415-
e -> (e.isFirstNormalInstallment() && DateUtils.isDateInRangeInclusive(date, e.getFromDate(), e.getDueDate()))
1416-
|| DateUtils.isDateInRangeFromExclusiveToInclusive(date, e.getFromDate(), e.getDueDate()));
1413+
public LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(final LocalDate date) {
1414+
return getRepaymentScheduleInstallment(e -> (DateUtils.isDateInRangeFromExclusiveToInclusive(date, e.getFromDate(), e.getDueDate())
1415+
|| (e.isFirstNormalInstallment(getRepaymentScheduleInstallments())
1416+
&& DateUtils.isDateInRangeInclusive(date, e.getFromDate(), e.getDueDate()))));
1417+
}
1418+
1419+
public List<LoanRepaymentScheduleInstallment> getInstallmentsUpToTransactionDate(final LocalDate transactionDate) {
1420+
return getRepaymentScheduleInstallments().stream()
1421+
.filter(i -> (transactionDate.isAfter(i.getFromDate())
1422+
|| (i.isFirstNormalInstallment(getRepaymentScheduleInstallments()) && !transactionDate.isBefore(i.getFromDate()))))
1423+
.collect(Collectors.toCollection(ArrayList::new));
14171424
}
14181425

14191426
public LoanRepaymentScheduleInstallment fetchRepaymentScheduleInstallment(final Integer installmentNumber) {

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,8 +1216,7 @@ private BigDecimal setScaleAndDefaultToNullIfZero(final BigDecimal value) {
12161216
return value.setScale(6, MoneyHelper.getRoundingMode());
12171217
}
12181218

1219-
public boolean isFirstNormalInstallment() {
1220-
return loan.getRepaymentScheduleInstallments().stream().filter(rp -> !rp.isDownPayment()).findFirst().stream()
1221-
.anyMatch(rp -> rp.equals(this));
1219+
public boolean isFirstNormalInstallment(List<LoanRepaymentScheduleInstallment> installments) {
1220+
return installments.stream().filter(rp -> !rp.isDownPayment()).findFirst().stream().anyMatch(rp -> rp.equals(this));
12221221
}
12231222
}

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2255,4 +2255,8 @@ public void updateVariationDays(final long daysToAdd) {
22552255
this.variationDays += daysToAdd;
22562256
}
22572257

2258+
public boolean isInterestRecognitionOnDisbursementDate() {
2259+
return this.interestRecognitionOnDisbursementDate;
2260+
}
2261+
22582262
}

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1861,10 +1861,10 @@ private void handleAccelerateMaturityDate(final LoanTransaction loanTransaction,
18611861
final Loan loan = loanTransaction.getLoan();
18621862
final LoanRepaymentScheduleInstallment currentInstallment = loan.getRelatedRepaymentScheduleInstallment(transactionDate);
18631863

1864-
if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate())) {
1864+
if (!installments.isEmpty() && transactionDate.isBefore(loan.getMaturityDate()) && currentInstallment != null) {
18651865
if (currentInstallment.isNotFullyPaidOff()) {
18661866
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx
1867-
&& loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
1867+
&& loan.isInterestBearingAndInterestRecalculationEnabled()) {
18681868
final BigDecimal interestOutstanding = currentInstallment.getInterestOutstanding(loan.getCurrency()).getAmount();
18691869
final BigDecimal newInterest = emiCalculator.getPeriodInterestTillDate(progressiveTransactionCtx.getModel(),
18701870
currentInstallment.getDueDate(), transactionDate, true).getAmount();
@@ -1918,9 +1918,8 @@ private void handleAccelerateMaturityDate(final LoanTransaction loanTransaction,
19181918
MathUtil.nullToZero(currentInstallment.getTotalPaidInAdvance()).add(futureTotalPaidInAdvance));
19191919
}
19201920

1921-
final List<LoanRepaymentScheduleInstallment> installmentsUpToTransactionDate = installments.stream()
1922-
.filter(installment -> transactionDate.isAfter(installment.getFromDate()))
1923-
.collect(Collectors.toCollection(ArrayList::new));
1921+
final List<LoanRepaymentScheduleInstallment> installmentsUpToTransactionDate = loan
1922+
.getInstallmentsUpToTransactionDate(transactionDate);
19241923

19251924
final List<LoanTransaction> transactionsToBeReprocessed = loan.getLoanTransactions().stream()
19261925
.filter(transaction -> transaction.getTransactionDate().isBefore(transactionDate))

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ public ChangedTransactionDetail reprocessLoanTransactions(String transactionProc
115115
if (loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor advancedProcessor) {
116116
LocalDate currentDate = DateUtils.getBusinessLocalDate();
117117
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> result = advancedProcessor
118-
.reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges);
118+
.reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments,
119+
charges);
119120
if (!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
120121
modelRepository.writeInterestScheduleModel(getLoan(loanTransactions, installments, charges), result.getRight());
121122
}

integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanContractTerminationTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@
1818
*/
1919
package org.apache.fineract.integrationtests;
2020

21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
2123
import java.math.BigDecimal;
2224
import java.util.concurrent.atomic.AtomicReference;
25+
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
2326
import org.apache.fineract.client.models.PostClientsResponse;
2427
import org.apache.fineract.client.models.PostLoanProductsResponse;
2528
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
2629
import org.apache.fineract.client.util.CallFailedRuntimeException;
2730
import org.apache.fineract.integrationtests.common.ClientHelper;
31+
import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
2832
import org.apache.fineract.integrationtests.common.Utils;
2933
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
3034
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
@@ -118,4 +122,31 @@ public void testNegativeLoanContractTerminationInNoProgressiveLoan() {
118122
});
119123
}
120124

125+
@Test
126+
public void testLoanContractTerminationSameDisbursementDate() {
127+
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
128+
final GlobalConfigurationHelper globalConfigurationHelper = new GlobalConfigurationHelper();
129+
130+
runAt("1 January 2024", () -> {
131+
132+
PostLoanProductsResponse loanProductsResponse = loanProductHelper
133+
.createLoanProduct(create4IProgressive().interestRecognitionOnDisbursementDate(false));
134+
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
135+
500.0, 7.0, 6, (request) -> request.interestRecognitionOnDisbursementDate(false));
136+
137+
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
138+
139+
loanTransactionHelper.moveLoanState(loanId,
140+
new PostLoansLoanIdRequest().note("Contract Termination Test").externalId(Utils.randomStringGenerator("", 20)),
141+
"contractTermination");
142+
143+
verifyTransactions(loanId, //
144+
transaction(100.0, "Disbursement", "01 January 2024"), //
145+
transaction(100.0, "Contract Termination", "01 January 2024"));
146+
147+
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
148+
assertEquals(BigDecimal.ZERO.stripTrailingZeros(), loanDetails.getSummary().getInterestCharged().stripTrailingZeros());
149+
});
150+
}
151+
121152
}

0 commit comments

Comments
 (0)