FINERACT-2308: Allow approval / disbursal above loan applied amount for multidisbursal progressive loan that does expect tranches
This commit is contained in:
committed by
Adam Saghy
parent
cb4c66302a
commit
68bfd7a3f8
+1
@@ -132,6 +132,7 @@ public enum DefaultLoanProduct implements LoanProduct {
|
||||
LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_CAPITALIZED_INCOME, //
|
||||
LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES, //
|
||||
LP2_PROGRESSIVE_ADVANCED_PAYMENT_ALLOCATION_BUYDOWN_FEES_CHARGE_OFF_REASON, //
|
||||
LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_FEE, //
|
||||
|
||||
+37
@@ -3426,6 +3426,43 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
|
||||
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_MULTIDISBURSAL_EXPECTS_TRANCHES,
|
||||
responseLoanProductMultidisbursalExpectTranches);
|
||||
|
||||
// LP2 with progressive loan schedule + horizontal + interest EMI + 360/30
|
||||
// + interest recalculation, preClosureInterestCalculationStrategy= till preclose
|
||||
// Frequency for recalculate Outstanding Principal: Daily, Frequency Interval for recalculation: 1
|
||||
// allow approved/disbursed amount over applied amount is enabled with percentage
|
||||
// multidisbursal loan that expects tranches
|
||||
// type
|
||||
final String name131 = DefaultLoanProduct.LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES
|
||||
.getName();
|
||||
final PostLoanProductsRequest loanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches = loanProductsRequestFactory
|
||||
.defaultLoanProductsRequestLP2EmiCapitalizedIncome()//
|
||||
.name(name131)//
|
||||
.daysInYearType(DaysInYearType.DAYS360.value)//
|
||||
.daysInMonthType(DaysInMonthType.DAYS30.value)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.preClosureInterestCalculationStrategy(1)//
|
||||
.rescheduleStrategyMethod(4)//
|
||||
.interestRecalculationCompoundingMethod(0)//
|
||||
.recalculationRestFrequencyType(2)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.paymentAllocation(List.of(//
|
||||
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
|
||||
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
|
||||
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
|
||||
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT"))) //
|
||||
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||
.overAppliedCalculationType(OverAppliedCalculationType.PERCENTAGE.value)//
|
||||
.overAppliedNumber(50)//
|
||||
.multiDisburseLoan(true)//
|
||||
.disallowExpectedDisbursements(false)//
|
||||
.maxTrancheCount(10)//
|
||||
.outstandingLoanBalance(10000.0);//
|
||||
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches = loanProductsApi
|
||||
.createLoanProduct(loanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches)
|
||||
.execute();
|
||||
TestContext.INSTANCE.set(
|
||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES,
|
||||
responseLoanProductsRequestLP2ProgressiveAdvPymnt36030InterestRecalcMultidisbApprovedOverAppliedExpectTranches);
|
||||
}
|
||||
|
||||
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
|
||||
|
||||
+1
@@ -255,4 +255,5 @@ public abstract class TestContextKey {
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_PERCENT_AMOUNT_INTEREST_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeAmountPlusPercentageInterestCharges";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_ALL_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeAllCharges";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INSTALLMENT_FEE_FLAT_INTEREST_CHARGES_TRANCHE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInstallmentFeeFlatPlusPercentageInterestChargesMultidisbursal";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentInterestRecalculationMultidisbursalApprovedOverAppliedAmountExpectedTransches";
|
||||
}
|
||||
|
||||
@@ -251,3 +251,17 @@ Feature: LoanProduct
|
||||
Then Loan Product response contains Buy Down Fees flag "false"
|
||||
Then Loan Details response contains Buy Down Fees flag "false"
|
||||
|
||||
@TestRailId:C3884
|
||||
Scenario: As a user I would like to verify multi-disburse loan product with over-applied amount and expected tranches can be created
|
||||
When Admin sets the business date to "1 January 2024"
|
||||
And Admin creates a client with random data
|
||||
When Admin creates a fully customized loan with disbursements details and following data:
|
||||
| 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 | 1st_tranche_disb_expected_date |1st_tranche_disb_principal | 2nd_tranche_disb_expected_date | 2nd_tranche_disb_principal |
|
||||
| LP2_PROGRESSIVE_ADV_PYMNT_INTEREST_RECALC_360_30_MULTIDISB_OVER_APPLIED_EXPECTED_TRANCHES | 01 January 2024 | 1000 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 700.0 | 02 January 2024 | 300.0 |
|
||||
And Admin successfully approves the loan on "1 January 2024" with "1000" amount and expected disbursement date on "1 January 2024"
|
||||
And Admin successfully disburse the loan on "1 January 2024" with "1000" EUR transaction amount
|
||||
Then Loan status will be "ACTIVE"
|
||||
When Admin sets the business date to "2 January 2024"
|
||||
And Admin successfully disburse the loan on "2 January 2024" with "300" EUR transaction amount
|
||||
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "02 January 2024" with "200" EUR transaction amount
|
||||
|
||||
|
||||
+5
-1
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.delinquency.service;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EARLIEST_UNPAID_DATE;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.NEXT_UNPAID_DUE_DATE;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
@@ -148,7 +149,10 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
||||
// Overpaid
|
||||
// loans
|
||||
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
|
||||
collectionData.setAvailableDisbursementAmount(loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount()));
|
||||
// Ensure availableDisbursementAmount is never negative
|
||||
final BigDecimal availableAmount = loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount());
|
||||
collectionData
|
||||
.setAvailableDisbursementAmount(availableAmount.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : availableAmount);
|
||||
collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
|
||||
|
||||
final LoanTransaction lastPayment = loan.getLastPaymentTransaction();
|
||||
|
||||
-8
@@ -414,14 +414,6 @@ public class LoanProduct extends AbstractPersistableCustom<Long> {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.allowApprovedDisbursedAmountsOverApplied) {
|
||||
if (this.isMultiDisburseLoan() && !this.disallowExpectedDisbursements) {
|
||||
throw new LoanProductGeneralRuleException(
|
||||
"disallowExpectedDisbursements.not.set.allowApprovedDisbursedAmountsOverApplied.cant.be.set",
|
||||
"Disallow Expected Disbursals Not Set - Allow Approved / Disbursed Amounts Over Applied Can't Be Set");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.overAppliedCalculationType == null || this.overAppliedCalculationType.isEmpty()) {
|
||||
if (this.allowApprovedDisbursedAmountsOverApplied) {
|
||||
throw new LoanProductGeneralRuleException(
|
||||
|
||||
@@ -28,10 +28,8 @@ Feature: Loan Product Validation
|
||||
Examples:
|
||||
| allowMultipleDisbursal | disallowExpectedDisbursements | allowApprovedDisbursedAmountsOverApplied | overAppliedCalculationType | overAppliedNumber | exception | template |
|
||||
| false | true | false | - | -1 | org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException | error.msg.allowMultipleDisbursals.not.set.disallowExpectedDisbursements.cant.be.set |
|
||||
| true | false | true | - | -1 | org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException | error.msg.disallowExpectedDisbursements.not.set.allowApprovedDisbursedAmountsOverApplied.cant.be.set |
|
||||
| true | true | true | - | -1 | org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException | error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedCalculationType.is.mandatory |
|
||||
| true | true | true | - | -1 | org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException | error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedCalculationType.is.mandatory |
|
||||
| true | false | true | - | -1 | org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException | error.msg.disallowExpectedDisbursements.not.set.allowApprovedDisbursedAmountsOverApplied.cant.be.set |
|
||||
| true | true | true | notflat | -1 | org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException | error.msg.overAppliedCalculationType.must.be.percentage.or.flat |
|
||||
| true | true | false | flat | -1 | org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException | error.msg.allowApprovedDisbursedAmountsOverApplied.is.not.set.overAppliedCalculationType.cant.be.entered |
|
||||
| true | true | true | flat | -1 | org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException | error.msg.allowApprovedDisbursedAmountsOverApplied.is.set.overAppliedNumber.is.mandatory |
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.integrationtests;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansDisbursementData;
|
||||
import org.apache.fineract.client.models.PutLoanProductsProductIdRequest;
|
||||
import org.apache.fineract.client.models.PutLoanProductsProductIdResponse;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class LoanProductOverAppliedAmountTest extends BaseLoanIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void testCreateMultiDisburseLoanProductWithOverAppliedAmountAndExpectedTranches() {
|
||||
runAt("01 January 2024", () -> {
|
||||
// Create Loan Product with multi-disburse, expected tranches, and over-applied amount
|
||||
final PostLoanProductsRequest loanProductRequest = create4IProgressive().multiDisburseLoan(true).maxTrancheCount(5)
|
||||
.outstandingLoanBalance(10000.0).disallowExpectedDisbursements(false) // Expected tranches enabled
|
||||
.allowApprovedDisbursedAmountsOverApplied(true).overAppliedCalculationType("percentage").overAppliedNumber(50);
|
||||
|
||||
// This should not throw an exception
|
||||
final PostLoanProductsResponse loanProductResponse = assertDoesNotThrow(
|
||||
() -> loanProductHelper.createLoanProduct(loanProductRequest));
|
||||
|
||||
assertNotNull(loanProductResponse);
|
||||
assertNotNull(loanProductResponse.getResourceId());
|
||||
|
||||
// Retrieve the created loan product to verify settings
|
||||
final GetLoanProductsProductIdResponse retrievedProduct = loanProductHelper
|
||||
.retrieveLoanProductById(loanProductResponse.getResourceId());
|
||||
|
||||
// Verify the loan product was created with correct settings
|
||||
assertEquals(true, retrievedProduct.getMultiDisburseLoan());
|
||||
assertEquals(false, retrievedProduct.getDisallowExpectedDisbursements());
|
||||
assertEquals(true, retrievedProduct.getAllowApprovedDisbursedAmountsOverApplied());
|
||||
assertEquals("percentage", retrievedProduct.getOverAppliedCalculationType());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testModifyMultiDisburseLoanProductWithOverAppliedAmountAndExpectedTranches() {
|
||||
runAt("01 January 2024", () -> {
|
||||
// Create initial loan product without over-applied amount
|
||||
final PostLoanProductsRequest initialLoanProductRequest = create4IProgressive().multiDisburseLoan(true).maxTrancheCount(5)
|
||||
.outstandingLoanBalance(10000.0).disallowExpectedDisbursements(false).allowApprovedDisbursedAmountsOverApplied(false)
|
||||
.overAppliedCalculationType(null).overAppliedNumber(null);
|
||||
|
||||
final PostLoanProductsResponse initialLoanProductResponse = loanProductHelper.createLoanProduct(initialLoanProductRequest);
|
||||
final Long loanProductId = initialLoanProductResponse.getResourceId();
|
||||
|
||||
// Modify loan product to enable over-applied amount
|
||||
final PutLoanProductsProductIdRequest modifyRequest = new PutLoanProductsProductIdRequest()
|
||||
.allowApprovedDisbursedAmountsOverApplied(true).overAppliedCalculationType("flat").overAppliedNumber(200).locale("en");
|
||||
|
||||
final PutLoanProductsProductIdResponse modifyResponse = assertDoesNotThrow(
|
||||
() -> loanProductHelper.updateLoanProductById(loanProductId, modifyRequest));
|
||||
|
||||
assertNotNull(modifyResponse);
|
||||
|
||||
// Retrieve the updated loan product to verify settings
|
||||
final GetLoanProductsProductIdResponse retrievedProduct = loanProductHelper.retrieveLoanProductById(loanProductId);
|
||||
assertEquals(true, retrievedProduct.getMultiDisburseLoan());
|
||||
assertEquals(false, retrievedProduct.getDisallowExpectedDisbursements());
|
||||
assertEquals(true, retrievedProduct.getAllowApprovedDisbursedAmountsOverApplied());
|
||||
assertEquals("flat", retrievedProduct.getOverAppliedCalculationType());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAvailableDisbursementAmountNotNegativeWhenDisbursedAmountExceedsApprovedAmount() {
|
||||
runAt("01 January 2024", () -> {
|
||||
// Create Loan Product with over-applied amount enabled
|
||||
final PostLoanProductsRequest loanProductRequest = create4IProgressive().multiDisburseLoan(true).maxTrancheCount(5)
|
||||
.outstandingLoanBalance(10000.0).disallowExpectedDisbursements(false) // Expected tranches enabled
|
||||
.allowApprovedDisbursedAmountsOverApplied(true).overAppliedCalculationType("percentage").overAppliedNumber(50);
|
||||
|
||||
final PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductRequest);
|
||||
final Long loanProductId = loanProductResponse.getResourceId();
|
||||
|
||||
// Create client
|
||||
final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
assertNotNull(clientId);
|
||||
|
||||
// Create and approve loan with amount 1000
|
||||
final Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "1 January 2024", 1000.0, 7.0, 6,
|
||||
request -> request.disbursementData(List.of(new PostLoansDisbursementData().expectedDisbursementDate("1 January 2024")
|
||||
.principal(BigDecimal.valueOf(1000.0)))));
|
||||
|
||||
// Disburse loan with amount 1500 (exceeds approved amount, but allowed due to over-applied setting)
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1500.0), "01 January 2024");
|
||||
|
||||
// Verify loan is active
|
||||
verifyLoanStatus(loanId, LoanStatus.ACTIVE);
|
||||
|
||||
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||
assertNotNull(loanDetails);
|
||||
|
||||
// Verify that the loan was created and disbursed successfully
|
||||
assert loanDetails.getStatus() != null;
|
||||
assertEquals(Boolean.TRUE, loanDetails.getStatus().getActive());
|
||||
|
||||
// Verify the amounts
|
||||
assert loanDetails.getApprovedPrincipal() != null;
|
||||
assertEquals(BigDecimal.valueOf(1000.0), loanDetails.getApprovedPrincipal().setScale(1, RoundingMode.HALF_UP));
|
||||
assert loanDetails.getDisbursementDetails() != null;
|
||||
double disbursementPrincipalSum = loanDetails.getDisbursementDetails().stream().mapToDouble(detail -> {
|
||||
assert detail.getPrincipal() != null;
|
||||
return detail.getPrincipal();
|
||||
}).sum();
|
||||
assertEquals(1500.0, disbursementPrincipalSum);
|
||||
|
||||
// The key test: availableDisbursementAmount should be 0 (not negative)
|
||||
// since disbursed amount (1500) > approved amount (1000)
|
||||
assert loanDetails.getDelinquent() != null;
|
||||
final BigDecimal availableDisbursementAmount = loanDetails.getDelinquent().getAvailableDisbursementAmount();
|
||||
assertNotNull(availableDisbursementAmount);
|
||||
assertTrue(availableDisbursementAmount.compareTo(BigDecimal.ZERO) >= 0,
|
||||
"availableDisbursementAmount should not be negative. Expected >= 0, but was: " + availableDisbursementAmount);
|
||||
assertEquals(BigDecimal.ZERO, availableDisbursementAmount,
|
||||
"availableDisbursementAmount should be 0 when disbursed amount exceeds approved amount");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user