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 67002b0d99d..ab434a921aa 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 @@ -1223,6 +1223,8 @@ private PostLoansRequest() {} public Integer graceOnArrearsAgeing; @Schema(example = "HORIZONTAL") public String loanScheduleProcessingType; + @Schema(example = "false") + public Boolean enableInstallmentLevelDelinquency; } @Schema(description = "PostLoansResponse") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java index 2c1251f4914..941dc8d1aee 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationCommandFromApiJsonHelper.java @@ -104,7 +104,8 @@ public final class LoanApplicationCommandFromApiJsonHelper { LoanApiConstants.lastApplication, // glim specific LoanApiConstants.daysInYearTypeParameterName, LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanApiConstants.DISALLOW_EXPECTED_DISBURSEMENTS, LoanApiConstants.FRAUD_ATTRIBUTE_NAME, - LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH)); + LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH, + LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY)); public static final String LOANAPPLICATION_UNDO = "loanapplication.undo"; private final FromJsonHelper fromApiJsonHelper; @@ -581,6 +582,22 @@ public void validateForCreate(final String json, final boolean isMeetingMandator } validatePartialPeriodSupport(interestCalculationPeriodType, baseDataValidator, element, loanProduct); + + // validate enable installment level delinquency + if (this.fromApiJsonHelper.parameterExists(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element)) { + final Boolean isEnableInstallmentLevelDelinquency = this.fromApiJsonHelper + .extractBooleanNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element); + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY) + .value(isEnableInstallmentLevelDelinquency).validateForBooleanValue(); + if (loanProduct.getDelinquencyBucket() == null) { + if (isEnableInstallmentLevelDelinquency) { + baseDataValidator.reset().parameter(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY).failWithCode( + "can.be.enabled.for.loan.with.loan.product.having.valid.delinquency.bucket", + "Installment level delinquency cannot be enabled for a loan if Delinquency bucket is not configured for loan product"); + } + } + } + if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index 36b9e86de6e..708155759e3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -327,7 +327,13 @@ private Loan assembleApplication(final JsonElement element, final Long clientId, } } - loanApplication.updateEnableInstallmentLevelDelinquency(loanProduct.isEnableInstallmentLevelDelinquency()); + final Boolean isEnableInstallmentLevelDelinquency = this.fromApiJsonHelper + .extractBooleanNamed(LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, element); + if (isEnableInstallmentLevelDelinquency != null) { + loanApplication.updateEnableInstallmentLevelDelinquency(isEnableInstallmentLevelDelinquency); + } else { + loanApplication.updateEnableInstallmentLevelDelinquency(loanProduct.isEnableInstallmentLevelDelinquency()); + } final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(element); final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstallmentLevelDelinquencyAPIIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstallmentLevelDelinquencyAPIIntegrationTests.java index fb9183169cd..d6ed9adc7de 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstallmentLevelDelinquencyAPIIntegrationTests.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/InstallmentLevelDelinquencyAPIIntegrationTests.java @@ -31,10 +31,12 @@ 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.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.SchedulerJobHelper; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -242,6 +244,172 @@ public void testInstallmentLevelDelinquencyUpdatedWhenCOBIsExecuted() { }); } + @Test + public void testInstallmentLevelDelinquencyTurnedOnForProductAndOffForLoan() { + runAt("31 May 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + // Create DelinquencyBuckets + Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(// + Pair.of(1, 10), // + Pair.of(11, 30), // + Pair.of(31, 60), // + Pair.of(61, null)// + )); + + // Create Loan Product + PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct( + InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS); + // set installment level delinquency as true + loanProductsRequest.setEnableInstallmentLevelDelinquency(true); + loanProductsRequest.setDelinquencyBucketId(delinquencyBucketId.longValue()); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest); + + // Apply and Approve Loan, turn loan level installment delinquency as false + Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4, + req -> req.setEnableInstallmentLevelDelinquency(false)); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023"); + + // Verify Repayment Schedule and Due Dates + verifyRepaymentSchedule(loanId, // + installment(1250.0, null, "01 January 2023"), // + installment(312.0, false, "31 January 2023"), // 120 days delinquent -> range4 + installment(312.0, false, "02 March 2023"), // 90 days delinquent -> range4 + installment(312.0, false, "01 April 2023"), // 60 days delinquent -> range3 + installment(314.0, false, "01 May 2023") // 30 days delinquent -> range2 + ); + + // since the installment level delinquency is overridden and set as false for loan application, therefore it + // is not calculated + verifyDelinquency(loanId, 120, "1250.0"); + }); + + } + + @Test + public void testInstallmentLevelDelinquencyTurnedOffForProductAndOnForLoan() { + runAt("31 May 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + // Create DelinquencyBuckets + Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(// + Pair.of(1, 10), // + Pair.of(11, 30), // + Pair.of(31, 60), // + Pair.of(61, null)// + )); + + // Create Loan Product + PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct( + InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS); + // set installment level delinquency as false + loanProductsRequest.setEnableInstallmentLevelDelinquency(false); + loanProductsRequest.setDelinquencyBucketId(delinquencyBucketId.longValue()); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest); + + // Apply and Approve Loan, turn loan level installment delinquency as true + Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4, + req -> req.setEnableInstallmentLevelDelinquency(true)); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023"); + + // Verify Repayment Schedule and Due Dates + verifyRepaymentSchedule(loanId, // + installment(1250.0, null, "01 January 2023"), // + installment(312.0, false, "31 January 2023"), // 120 days delinquent -> range4 + installment(312.0, false, "02 March 2023"), // 90 days delinquent -> range4 + installment(312.0, false, "01 April 2023"), // 60 days delinquent -> range3 + installment(314.0, false, "01 May 2023") // 30 days delinquent -> range2 + ); + + // since the installment level delinquency is overridden and set as true for loan application, therefore it + // is calculated + verifyDelinquency(loanId, 120, "1250.0", // + delinquency(11, 30, "314.0"), // 4th installment + delinquency(31, 60, "312.0"), // 3rd installment + delinquency(61, null, "624.0") // 1st installment + 2nd installment + ); + }); + + } + + @Test + public void testLoanInheritsInstallmentLevelSettingFromLoanProductIfNotSet() { + runAt("31 May 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + // Create DelinquencyBuckets + Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec, List.of(// + Pair.of(1, 10), // + Pair.of(11, 30), // + Pair.of(31, 60), // + Pair.of(61, null)// + )); + + // Create Loan Product + PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct( + InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS); + // set installment level delinquency as true + loanProductsRequest.setEnableInstallmentLevelDelinquency(true); + loanProductsRequest.setDelinquencyBucketId(delinquencyBucketId.longValue()); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest); + + // Apply and Approve Loan, do not set installment level delinquency + Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4); + + // Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023"); + + // Verify Repayment Schedule and Due Dates + verifyRepaymentSchedule(loanId, // + installment(1250.0, null, "01 January 2023"), // + installment(312.0, false, "31 January 2023"), // 120 days delinquent -> range4 + installment(312.0, false, "02 March 2023"), // 90 days delinquent -> range4 + installment(312.0, false, "01 April 2023"), // 60 days delinquent -> range3 + installment(314.0, false, "01 May 2023") // 30 days delinquent -> range2 + ); + + // since the installment level delinquency is inherited from loan product, therefore it + // is calculated + verifyDelinquency(loanId, 120, "1250.0", // + delinquency(11, 30, "314.0"), // 4th installment + delinquency(31, 60, "312.0"), // 3rd installment + delinquency(61, null, "624.0") // 1st installment + 2nd installment + ); + }); + + } + + @Test + public void tesInstallmentLevelSettingForLoanWithLoanProductWithoutDelinquencyBucketValidation() { + + runAt("31 May 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + // Create Loan Product + PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct( + InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS); + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest); + + // Apply For Loan with installment level delinquency setting + CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, + () -> loanTransactionHelper.applyLoan(applyLoanRequest(clientId, loanProductResponse.getResourceId(), "01 January 2023", + 1250.0, 4, req -> req.setEnableInstallmentLevelDelinquency(true)))); + + Assertions.assertTrue(callFailedRuntimeException.getMessage().contains( + "Installment level delinquency cannot be enabled for a loan if Delinquency bucket is not configured for loan product")); + + }); + + } + private void updateBusinessDateAndExecuteCOBJob(String date) { businessDateHelper.updateBusinessDate( new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));