FINERACT-2308: Allow approval and disbursal above loan applied amount with non multibursal loan product

This commit is contained in:
Jose Alberto Hernandez
2025-06-18 15:15:28 +02:00
committed by Adam Saghy
parent 38a5686df8
commit bb29932658
5 changed files with 82 additions and 20 deletions
@@ -410,7 +410,7 @@ public class LoanProduct extends AbstractPersistableCustom<Long> {
}
if (this.allowApprovedDisbursedAmountsOverApplied) {
if (!this.disallowExpectedDisbursements) {
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");
@@ -2124,7 +2124,7 @@ public final class LoanApplicationValidator {
}
private void compareApprovedToProposedPrincipal(Loan loan, BigDecimal approvedLoanAmount) {
if (loan.loanProduct().isDisallowExpectedDisbursements() && loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
BigDecimal maxApprovedLoanAmount = getOverAppliedMax(loan);
if (approvedLoanAmount.compareTo(maxApprovedLoanAmount) > 0) {
final String errorMessage = "Loan approved amount can't be greater than maximum applied loan amount calculation.";
@@ -37,26 +37,34 @@ public final class LoanDisbursementValidator {
final BigDecimal totalDisbursed) {
final BigDecimal capitalizedIncome = loan.getSummary().getTotalCapitalizedIncome();
if (loan.loanProduct().isDisallowExpectedDisbursements() && loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
final BigDecimal maxDisbursedAmount = loanApplicationValidator.getOverAppliedMax(loan);
if (totalDisbursed.add(capitalizedIncome).compareTo(maxDisbursedAmount) > 0) {
final String errorMessage = String.format(
"Loan disbursal amount can't be greater than maximum applied loan amount calculation. "
+ "Total disbursed amount: %s Maximum disbursal amount: %s",
totalDisbursed.stripTrailingZeros().toPlainString(), maxDisbursedAmount.stripTrailingZeros().toPlainString());
throw new InvalidLoanStateTransitionException("disbursal",
"amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, disbursedAmount,
maxDisbursedAmount);
}
validateOverMaximumAmount(loan, totalDisbursed, capitalizedIncome);
} else {
if ((totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0)
|| (totalDisbursed.add(capitalizedIncome).compareTo(loan.getApprovedPrincipal()) > 0)) {
final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved principal ";
throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed,
loan.getApprovedPrincipal());
if (loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
validateOverMaximumAmount(loan, disbursedAmount, capitalizedIncome);
} else {
if ((totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0)
|| (totalDisbursed.add(capitalizedIncome).compareTo(loan.getApprovedPrincipal()) > 0)) {
final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved principal ";
throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed,
loan.getApprovedPrincipal());
}
}
}
}
public void validateOverMaximumAmount(final Loan loan, final BigDecimal totalDisbursed, final BigDecimal capitalizedIncome) {
final BigDecimal maxDisbursedAmount = loanApplicationValidator.getOverAppliedMax(loan);
if (totalDisbursed.add(capitalizedIncome).compareTo(maxDisbursedAmount) > 0) {
final String errorMessage = String.format(
"Loan disbursal amount can't be greater than maximum applied loan amount calculation. "
+ "Total disbursed amount: %s Maximum disbursal amount: %s",
totalDisbursed.stripTrailingZeros().toPlainString(), maxDisbursedAmount.stripTrailingZeros().toPlainString());
throw new InvalidLoanStateTransitionException("disbursal",
"amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, totalDisbursed,
maxDisbursedAmount);
}
}
public void validateDisburseAmountNotExceedingApprovedAmount(final Loan loan, final BigDecimal diff,
final BigDecimal principalDisbursed) {
if (!loan.loanProduct().isMultiDisburseLoan() && diff.compareTo(BigDecimal.ZERO) < 0) {
@@ -146,10 +146,10 @@ public class LoanDisbursementService {
disbursementDetails.updatePrincipal(principalDisbursed);
}
}
BigDecimal totalAmount = BigDecimal.ZERO;
if (loan.loanProduct().isMultiDisburseLoan()) {
Collection<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails();
BigDecimal setPrincipalAmount = BigDecimal.ZERO;
BigDecimal totalAmount = BigDecimal.ZERO;
for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) {
if (disbursementDetails.actualDisbursementDate() != null) {
setPrincipalAmount = setPrincipalAmount.add(disbursementDetails.principal());
@@ -157,12 +157,11 @@ public class LoanDisbursementService {
totalAmount = totalAmount.add(disbursementDetails.principal());
}
loan.getLoanRepaymentScheduleDetail().setPrincipal(setPrincipalAmount);
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount);
} else {
loan.getLoanRepaymentScheduleDetail()
.setPrincipal(loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(diff).getAmount());
}
loanDisbursementValidator.validateDisburseAmountNotExceedingApprovedAmount(loan, diff, principalDisbursed);
loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount);
}
return disburseAmount;
}
@@ -5946,6 +5946,61 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
});
}
// UC155: Validate allowApprovedDisbursedAmountsOverApplied setting on Non MultiDisbursement Loan Product
// 1. Create a Loan product with allowApprovedDisbursedAmountsOverApplied in true using overAppliedCalculationType
// flat amount
// 2. Submit Loan with 1,000
// 3. Approve the Loan with 1,300
// 4. Disburse the Loan with 1,450
@Test
public void uc155() {
final String operationDate = "1 January 2024";
AtomicLong createdLoanId = new AtomicLong();
runAt(operationDate, () -> {
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(12.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).numberOfRepayments(4)//
.repaymentEvery(1)//
.repaymentFrequencyType(1L)//
.allowPartialPeriodInterestCalcualtion(false)//
.installmentAmountInMultiplesOf(null)//
.multiDisburseLoan(false)//
.disallowExpectedDisbursements(null)//
.allowApprovedDisbursedAmountsOverApplied(true)//
.overAppliedCalculationType("flat")//
.overAppliedNumber(500)//
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 1000.0, 4)
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy")//
.interestRatePerPeriod(BigDecimal.valueOf(12.0));
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
createdLoanId.set(loanResponse.getLoanId());
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()
.approvedLoanAmount(BigDecimal.valueOf(1300)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en"));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
assertTrue(loanDetails.getStatus().getWaitingForDisbursal());
assertEquals("1300.000000", loanDetails.getApprovedPrincipal().toString());
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class,
() -> loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en")
.transactionAmount(BigDecimal.valueOf(1550.0))));
Assertions.assertTrue(callFailedRuntimeException.getMessage()
.contains("Loan disbursal amount can't be greater than maximum applied loan amount calculation"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate)
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(1450.0)));
loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
assertTrue(loanDetails.getStatus().getActive());
assertEquals("1450.000000", loanDetails.getSummary().getPrincipalOutstanding().toString());
});
}
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");