From 5093ca73636a0e964d2a4beab2c4447177026488 Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Wed, 3 Jul 2024 00:40:12 -0600 Subject: [PATCH] FINERACT-2102: Loan Product enableDownPayment overrided in the Loan application --- .../api/LoansApiResourceSwagger.java | 4 + .../service/LoanScheduleAssembler.java | 23 ++- .../LoanApplicationValidator.java | 6 +- .../serialization/LoanScheduleValidator.java | 15 +- ...ntAllocationLoanRepaymentScheduleTest.java | 170 ++++++++++++++++++ .../loans/LoanApplicationTestBuilder.java | 10 ++ 6 files changed, 225 insertions(+), 3 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index 1e54f23bd..7808e0758 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -1245,6 +1245,8 @@ final class LoansApiResourceSwagger { public String loanScheduleProcessingType; @Schema(example = "false") public Boolean enableInstallmentLevelDelinquency; + @Schema(example = "false") + public Boolean enableDownPayment; } @Schema(description = "PostLoansResponse") @@ -1394,6 +1396,8 @@ final class LoansApiResourceSwagger { public String loanScheduleProcessingType; @Schema(example = "false") public Boolean enableInstallmentLevelDelinquency; + @Schema(example = "false") + public Boolean enableDownPayment; static final class PutLoansLoanIdChanges { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 716015764..4b7173df5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -466,7 +466,11 @@ public class LoanScheduleAssembler { final boolean isPrincipalCompoundingDisabledForOverdueLoans = this.configurationDomainService .isPrincipalCompoundingDisabledForOverdueLoans(); - final boolean isDownPaymentEnabled = loanProduct.getLoanProductRelatedDetail().isEnableDownPayment(); + boolean isDownPaymentEnabled = loanProduct.getLoanProductRelatedDetail().isEnableDownPayment(); + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_DOWN_PAYMENT, element)) { + isDownPaymentEnabled = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.ENABLE_DOWN_PAYMENT, element); + } + BigDecimal disbursedAmountPercentageForDownPayment = null; boolean isAutoRepaymentForDownPaymentEnabled = false; if (isDownPaymentEnabled) { @@ -1379,6 +1383,22 @@ public class LoanScheduleAssembler { changes.put(LoanProductConstants.IS_EQUAL_AMORTIZATION_PARAM, newValue); loanProductRelatedDetail.setEqualAmortization(newValue); } + + if (command.isChangeInBooleanParameterNamed(LoanProductConstants.ENABLE_DOWN_PAYMENT, + loanProductRelatedDetail.isEnableDownPayment())) { + final boolean newValue = command.booleanPrimitiveValueOfParameterNamed(LoanProductConstants.ENABLE_DOWN_PAYMENT); + changes.put(LoanProductConstants.ENABLE_DOWN_PAYMENT, newValue); + loanProductRelatedDetail.setEnableDownPayment(newValue); + if (!newValue) { + loanProductRelatedDetail.setEnableAutoRepaymentForDownPayment(false); + loanProductRelatedDetail.setDisbursedAmountPercentageForDownPayment(null); + } else { + loanProductRelatedDetail.setEnableAutoRepaymentForDownPayment( + loan.loanProduct().getLoanProductRelatedDetail().isEnableAutoRepaymentForDownPayment()); + loanProductRelatedDetail.setDisbursedAmountPercentageForDownPayment( + loan.loanProduct().getLoanProductRelatedDetail().getDisbursedAmountPercentageForDownPayment()); + } + } } public Pair> assembleLoanApproval(AppUser currentUser, JsonCommand command, Long loanId) { @@ -1446,4 +1466,5 @@ public class LoanScheduleAssembler { return Pair.of(loan, actualChanges); } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index c908df94a..55bd3d57a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -169,7 +169,7 @@ public final class LoanApplicationValidator { LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanApiConstants.DISALLOW_EXPECTED_DISBURSEMENTS, LoanApiConstants.FRAUD_ATTRIBUTE_NAME, LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH, - LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY)); + LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, LoanProductConstants.ENABLE_DOWN_PAYMENT)); public static final String LOANAPPLICATION_UNDO = "loanapplication.undo"; private final FromJsonHelper fromApiJsonHelper; @@ -741,6 +741,8 @@ public final class LoanApplicationValidator { } } + loanScheduleValidator.validateDownPaymentAttribute(loanProduct.getLoanProductRelatedDetail().isEnableDownPayment(), element); + checkForProductMixRestrictions(element); validateSubmittedOnDate(element, submittedOnDate, loanProduct); validateDisbursementDetails(loanProduct, element); @@ -1396,6 +1398,8 @@ public final class LoanApplicationValidator { } } + loanScheduleValidator.validateDownPaymentAttribute(loanProduct.getLoanProductRelatedDetail().isEnableDownPayment(), element); + validateDisbursementDetails(loanProduct, element); validateSubmittedOnDate(element, loan.getSubmittedOnDate(), loanProduct); validateClientOrGroup(client, group, productId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanScheduleValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanScheduleValidator.java index d9167b633..88c79f1aa 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanScheduleValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanScheduleValidator.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; @@ -71,7 +72,8 @@ public final class LoanScheduleValidator { LoanApiConstants.repaymentFrequencyDayOfWeekTypeParameterName, LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.datatables, LoanApiConstants.isEqualAmortizationParam, LoanProductConstants.RATES_PARAM_NAME, LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, - LoanProductConstants.FIXED_LENGTH, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY)); + LoanProductConstants.FIXED_LENGTH, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, + LoanProductConstants.ENABLE_DOWN_PAYMENT)); private final FromJsonHelper fromApiJsonHelper; @@ -176,4 +178,15 @@ public final class LoanScheduleValidator { dataValidationErrors.add(error); } } + + public void validateDownPaymentAttribute(final boolean isDownPaymentEnabledInLoanProduct, final JsonElement element) { + final Boolean inputIsDownPaymentEnabled = this.fromApiJsonHelper.extractBooleanNamed(LoanProductConstants.ENABLE_DOWN_PAYMENT, + element); + if (!isDownPaymentEnabledInLoanProduct && Boolean.TRUE.equals(inputIsDownPaymentEnabled)) { + throw new GeneralPlatformDomainRuleException("error.msg.downpayment.is.not.enabled.in.loan.product", + "The Loan can not override the downpayment flag because in the Loan Product the downpayment is disabled", + inputIsDownPaymentEnabled); + } + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 0ea2c32be..a85911696 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -57,6 +57,7 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionI import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest; +import org.apache.fineract.client.models.PutLoansLoanIdRequest; import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; import org.apache.fineract.integrationtests.common.BusinessDateHelper; @@ -77,6 +78,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -4113,6 +4115,174 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan }); } + // UC138: Advanced payment allocation, with DownPayment enabled in Loan Product but disabled in Loan application + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and DownPayment + // 2. Submit Loan without DownPayment and approve + // 3. Disburse only 10 + // 4. Check the repayment schedule is correct, withoutDownPayment installment + @Test + public void uc138() { + runAt("23 March 2024", () -> { + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .installmentAmountInMultiplesOf(null).numberOfRepayments(3).repaymentEvery(15).enableDownPayment(true) + .enableAutoRepaymentForDownPayment(true).disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(client.getClientId(), loanProductResponse.getResourceId(), + "23 March 2024", 1000.0, 4); + + applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(45) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15) + .enableDownPayment(false); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(10)).dateFormat(DATETIME_PATTERN).approvedOnDate("23 March 2024").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("23 March 2024").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(10.0)).locale("en")); + + // verify schedule (without DownPayment) + verifyRepaymentSchedule(loanResponse.getLoanId(), // + installment(10, null, "23 March 2024"), // + installment(3.33, 0, 0, 0, 3.33, false, "07 April 2024", 6.67), // + installment(3.33, 0, 0, 0, 3.33, false, "22 April 2024", 3.34), // + installment(3.34, 0, 0, 0, 3.34, false, "07 May 2024", 0.0) // + ); + }); + } + + // UC139a: Advanced payment allocation, with DownPayment enabled in Loan Product but disabled and enabled in Loan + // application + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and DownPayment + // 2. Submit Loan without DownPayment and approve + // 3. Modify Loan Application to enable DownPayment + // 4. Disburse only 10 + // 5. Check the repayment schedule is correct, with DownPayment installment + @Test + public void uc139a() { + final String processDate = "23 March 2024"; + final Double amount = 1000.0; + runAt(processDate, () -> { + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .installmentAmountInMultiplesOf(null).numberOfRepayments(3).repaymentEvery(15).enableDownPayment(true) + .enableAutoRepaymentForDownPayment(true).disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(client.getClientId(), loanProductResponse.getResourceId(), processDate, + amount, 4); + + applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(45) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15) + .enableDownPayment(false); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.modifyApplicationForLoan(loanResponse.getLoanId(), "modify", + new PutLoansLoanIdRequest().clientId(client.getClientId()).productId(loanProductResponse.getResourceId()) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .repaymentEvery(15).interestCalculationPeriodType(1).interestType(0).expectedDisbursementDate(processDate) + .principal(amount.longValue()).repaymentFrequencyType(0).numberOfRepayments(3).loanTermFrequency(45) + .loanTermFrequencyType(0).enableDownPayment(true).loanType("individual").dateFormat(DATETIME_PATTERN) + .locale("en")); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(10)).dateFormat(DATETIME_PATTERN).approvedOnDate(processDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(processDate) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(10.0)).locale("en")); + + // verify schedule (with DownPayment) + verifyRepaymentSchedule(loanResponse.getLoanId(), // + installment(10, null, "23 March 2024"), // + installment(2.5, 0, 0, 0, 0.0, true, "23 March 2024", 7.5), // + installment(2.5, 0, 0, 0, 2.5, false, "07 April 2024", 5.0), // + installment(2.5, 0, 0, 0, 2.5, false, "22 April 2024", 2.5), // + installment(2.5, 0, 0, 0, 2.5, false, "07 May 2024", 0.0) // + ); + }); + } + + // UC139b: Advanced payment allocation, with DownPayment enabled in Loan Product and enabled then disabled in Loan + // application + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and DownPayment + // 2. Submit Loan with DownPayment and approve + // 3. Modify Loan Application to disable DownPayment + // 4. Disburse only 10 + // 5. Check the repayment schedule is correct, without DownPayment installment + @Test + public void uc139b() { + final String processDate = "23 March 2024"; + final Double amount = 1000.0; + runAt(processDate, () -> { + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .installmentAmountInMultiplesOf(null).numberOfRepayments(3).repaymentEvery(15).enableDownPayment(true) + .enableAutoRepaymentForDownPayment(true).disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(client.getClientId(), loanProductResponse.getResourceId(), processDate, + amount, 4); + + applicationRequest = applicationRequest.numberOfRepayments(3).loanTermFrequency(45) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15) + .enableDownPayment(true); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.modifyApplicationForLoan(loanResponse.getLoanId(), "modify", + new PutLoansLoanIdRequest().clientId(client.getClientId()).productId(loanProductResponse.getResourceId()) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .repaymentEvery(15).interestCalculationPeriodType(1).interestType(0).expectedDisbursementDate(processDate) + .principal(amount.longValue()).repaymentFrequencyType(0).numberOfRepayments(3).loanTermFrequency(45) + .loanTermFrequencyType(0).enableDownPayment(false).loanType("individual").dateFormat(DATETIME_PATTERN) + .locale("en")); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(10)).dateFormat(DATETIME_PATTERN).approvedOnDate(processDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(processDate) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(10.0)).locale("en")); + + // verify schedule (without DownPayment) + verifyRepaymentSchedule(loanResponse.getLoanId(), // + installment(10, null, "23 March 2024"), // + installment(3.33, 0, 0, 0, 3.33, false, "07 April 2024", 6.67), // + installment(3.33, 0, 0, 0, 3.33, false, "22 April 2024", 3.34), // + installment(3.34, 0, 0, 0, 3.34, false, "07 May 2024", 0.0) // + ); + }); + } + + // uc140: Negative Test: Advanced payment allocation, with DownPayment disabled in Loan Product but try to enable in + // Loan application + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. and without DownPayment + // 2. Try to summit the Loan Application with DownPayment expecting and Exception + @Test + public void uc140() { + runAt("23 March 2024", () -> { + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .installmentAmountInMultiplesOf(null).numberOfRepayments(3).repaymentEvery(15).enableDownPayment(false); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(client.getClientId(), loanProductResponse.getResourceId(), + "23 March 2024", 1000.0, 3) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY).repaymentEvery(15) + .enableDownPayment(true); + + Long clientId = client.getClientId(); + CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper + .applyLoan(applyLoanRequest(clientId, loanProductResponse.getResourceId(), "23 March 2024", 1000.0, 3) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .repaymentEvery(15).enableDownPayment(true))); + + Assertions.assertTrue(callFailedRuntimeException.getMessage() + .contains("The Loan can not override the downpayment flag because in the Loan Product the downpayment is disabled")); + }); + } + @Test public void testChargebackTransactionForProgressiveWithLoanAdvancedPaymentAllocationStrategyAndChargebackAllocation() { runAt("15 January 2023", () -> { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java index c6e49ab9b..879d71f52 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java @@ -85,6 +85,7 @@ public class LoanApplicationTestBuilder { private String linkAccountId; private String inArrearsTolerance; private boolean createStandingInstructionAtDisbursement = false; + private boolean enableDownPayment = false; public String build(final String clientID, final String groupID, final String loanProductId, final String savingsID) { final HashMap map = new HashMap<>(); @@ -208,6 +209,9 @@ public class LoanApplicationTestBuilder { if (createStandingInstructionAtDisbursement == true) { map.put("createStandingInstructionAtDisbursement", true); } + if (enableDownPayment == true) { + map.put("enableDownPayment", enableDownPayment); + } LOG.info("Loan Application request : {} ", map); return new Gson().toJson(map); } @@ -440,4 +444,10 @@ public class LoanApplicationTestBuilder { this.createStandingInstructionAtDisbursement = true; return this; } + + public LoanApplicationTestBuilder withEnableDownPayment() { + this.enableDownPayment = true; + return this; + } + }